Storage bindings

Cache read-heavy data in KV

Use KV for read-heavy derived values while respecting eventual consistency and keeping D1 as the authoritative record.

Workers KV is a key-value store built for reads. A value written once gets copied to Cloudflare’s edge locations, so the next read from anywhere is fast. The price you pay is that a write takes time to reach every location. That model is called eventual consistency.

That makes KV a great cache and a bad counter. Configuration, feature flags, a rendered public list: perfect. Anything that needs the newest value right now, or a uniqueness guarantee: use D1 or a Durable Object.

Set it up

Create a namespace and bind it as CACHE:

npx wrangler kv namespace create CACHE
{
  "kv_namespaces": [
    { "binding": "CACHE", "id": "..." }
  ]
}

Run npx wrangler types again so env.CACHE is typed.

The public list of recent links is read far more than it’s written. Let’s cache it for a minute:

const cached = await env.CACHE.get('recent-links', 'json')
if (cached) return Response.json(cached)
const links = await listRecentLinks(env.DB)
await env.CACHE.put('recent-links', JSON.stringify(links), { expirationTtl: 60 })

Passing 'json' as the second argument to get() parses the value for you. expirationTtl is in seconds, and 60 is the minimum KV accepts.

Invalidation is best effort

After creating a link, delete the key so the next reader rebuilds the list. But understand what that does and doesn’t promise. A reader in another region may still get the old value for a short while. KV can even return a cached “not found” right after you wrote the key.

So the rule is: D1 stays authoritative, and the application must be correct even when KV is stale. If a page shows a link one minute late, nobody is hurt. If a payment counter is one minute late, someone is.

Version your keys

When the shape of the cached JSON changes, old entries can’t be decoded as the new format. Don’t try to migrate them. Change the key instead, recent-links:v2, and let the old ones expire.

Pick the TTL from staleness, not hit rate

A long TTL gives a great hit rate and a stale page. Ask how old the list is allowed to be, and set the TTL to that. For Link Vault, one minute is fine.

One more risk. When a popular key expires, many requests miss at once and all hit D1 together. That’s a cache stampede. It moves the load instead of removing it. For a small app, a short TTL keeps it harmless. For a big one, cap concurrent rebuilds or coordinate them through a Durable Object.

Now add the cache to your GET /api/links route. During local testing, log a safe line on each request, something like { route: '/api/links', cache: 'hit' } or 'miss'. Two requests in a row should show one miss and one hit.

Lesson completed