Config Caching Strategy
Store Shield uses a 4-layer caching strategy to serve protection configuration with minimal latency and cost. This document explains why and how.
The Problem
Every page load on a merchant's store needs to know:
- Which protections are enabled (right-click, copy, bot detection, etc.)
- IP blocking rules
- Current plan features
Naive approach: Query Fly.io database on every request
Problem: High latency, expensive, single point of failure
The Solution: 4-Layer Waterfall
Request arrives
│
▼
┌─────────────────────────────────────────────────────┐
│ Layer 1: MEMORY CACHE (Worker isolate) │
│ • Fastest (nanoseconds) │
│ • Per-worker, lives until isolate recycles │
│ • Hit rate: ~60-70% │
└─────────────────────────┬───────────────────────────┘
│ miss
▼
┌─────────────────────────────────────────────────────┐
│ Layer 2: KV CACHE (Cloudflare KV) │
│ • Fast (~10-50ms) │
│ • Global, 5-minute TTL │
│ • Hit rate: ~25-30% │
└─────────────────────────┬───────────────────────────┘
│ miss
▼
┌─────────────────────────────────────────────────────┐
│ Layer 3: D1 CACHE (Cloudflare D1) │
│ • Moderate (~20-100ms) │
│ • Edge-local, persistent │
│ • Hit rate: ~5-8% │
└─────────────────────────┬───────────────────────────┘
│ miss
▼
┌─────────────────────────────────────────────────────┐
│ Layer 4: SOURCE (Fly.io API) │
│ • Slowest (~100-300ms) │
│ • Always current, source of truth │
│ • Hit rate: ~1-2% │
└─────────────────────────────────────────────────────┘
Implementation
Layer 1: Memory Cache
// In-memory cache per Worker isolate
const configCache = new Map();
async function getConfig(shop) {
const cacheKey = `config:${shop}`;
// Layer 1: Memory
if (configCache.has(cacheKey)) {
const { config, timestamp } = configCache.get(cacheKey);
if (Date.now() - timestamp < 60_000) { // 1 minute
return config;
}
}
// Continue to Layer 2...
}
Layer 2: KV Cache
async function getConfig(shop, env) {
// ... Layer 1 miss ...
// Layer 2: KV
const kvKey = `merchant-config:${shop}`;
const kvConfig = await env.KV.get(kvKey, 'json');
if (kvConfig) {
// Populate Layer 1
configCache.set(cacheKey, { config: kvConfig, timestamp: Date.now() });
return kvConfig;
}
// Continue to Layer 3...
}
Layer 3: D1 Cache
async function getConfig(shop, env) {
// ... Layer 2 miss ...
// Layer 3: D1
const d1Config = await env.DB.prepare(
'SELECT * FROM MerchantConfig WHERE shop = ?'
).bind(shop).first();
if (d1Config) {
// Populate Layers 1 & 2
configCache.set(cacheKey, { config: d1Config, timestamp: Date.now() });
await env.KV.put(kvKey, JSON.stringify(d1Config), { expirationTtl: 300 });
return d1Config;
}
// Continue to Layer 4...
}
Layer 4: Source (Fly.io)
async function getConfig(shop, env) {
// ... Layer 3 miss ...
// Layer 4: Fly.io (source of truth)
const response = await fetch(`${FLY_API_URL}/api/protection-config?shop=${shop}`);
const sourceConfig = await response.json();
// Populate all layers
configCache.set(cacheKey, { config: sourceConfig, timestamp: Date.now() });
await env.KV.put(kvKey, JSON.stringify(sourceConfig), { expirationTtl: 300 });
await env.DB.prepare(
'INSERT OR REPLACE INTO MerchantConfig (shop, config, updated_at) VALUES (?, ?, ?)'
).bind(shop, JSON.stringify(sourceConfig), Date.now()).run();
return sourceConfig;
}
Cache Invalidation
On Settings Change
When a merchant updates settings:
// app/routes/api.protection.config.ts
export async function action({ request }) {
// 1. Save to Prisma (source of truth)
await prisma.merchantSettings.update({
where: { shop },
data: newSettings
});
// 2. Invalidate edge caches
await fetch(`${EVENTS_WORKER_URL}/api/invalidate-config`, {
method: 'POST',
body: JSON.stringify({ shop })
});
}
Heartbeat-Based Updates
The theme extension sends heartbeats that naturally refresh the cache:
// heartbeat-handler.js
export async function handleHeartbeat(request, env) {
const { shop, protections } = await request.json();
// Update D1 with current config
await env.DB.prepare(`
INSERT OR REPLACE INTO MerchantConfig (shop, protections, last_seen)
VALUES (?, ?, ?)
`).bind(shop, JSON.stringify(protections), Date.now()).run();
// KV will naturally expire and refresh
}
Cost Analysis
| Layer | Cost per Read | Reads/month (est.) | Monthly Cost |
|---|---|---|---|
| Memory | $0 | 6M | $0 |
| KV | $0.50/M | 2.5M | ~$1.25 |
| D1 | $0.001/M | 500K | ~$0.50 |
| Fly.io | ~$0.01/req | 100K | ~$1.00 |
| Total | ~$2.75 |
Without caching (all requests to Fly.io): ~$90/month
Savings: ~97%
TTL Strategy
| Layer | TTL | Reason |
|---|---|---|
| Memory | 1 minute | Fresh enough, isolate may recycle anyway |
| KV | 5 minutes | Balance freshness vs cost |
| D1 | Indefinite | Persistent backup, overwritten on update |
Trade-offs
Advantages
- Cost: 97% reduction in origin requests
- Latency: Most requests served in under 10ms
- Resilience: Works even if Fly.io is down (D1 fallback)
- Scalability: Edge handles any traffic spike
Disadvantages
- Staleness: Config changes take up to 5 minutes to propagate
- Complexity: 4 layers to debug
- Inconsistency: Different edge locations may have different cache states
Mitigation
- Explicit invalidation on settings change
- Heartbeat naturally refreshes active stores
- Graceful degradation with last-known config