The answer. Workers KV is a central store with a cache in every Cloudflare location. Reads are fast because they are cache hits; writes go to the center and invalidate outward, which is why a value can differ between two locations for up to a minute (or the cacheTtl you passed on read). That makes KV ideal for things read a thousand times per write: feature flags, signing keys, rendered fragments, per-tenant config. It makes KV wrong for anything that needs read-after-write or coordination: counters, locks, inventory, "was this token already used". Concurrent writes to one key are last-writer-wins, and the same key accepts one write per second before returning 429. When you need the strong version, that is a Durable Object; when you need SQL, that is D1.
The pattern.
await env.CACHE.put("jwks:v1", json, { expirationTtl: 3600, metadata: { fetchedAt: Date.now() } });
const { value, metadata } = await env.CACHE.getWithMetadata<{ fetchedAt: number }>("jwks:v1", { cacheTtl: 300 });
const keys = await env.CACHE.list({ prefix: "site:", limit: 100 }); // paginate with list_complete / cursor
| Fact | Value |
|---|---|
| Consistency | eventual; same location immediate, elsewhere up to 60 s or cacheTtl |
cacheTtl on reads |
min 30 s, default 60 s |
| Writes to one key | 1 per second (429 above) |
| Key / value / metadata | 512 B / 25 MiB / 1 KiB |
| Free plan per day | 100k reads, 1k writes, 1 GB stored |
Watch out.
cacheTtlpins a stale value in that location for the whole window, including negative lookups (a missing key stays "missing").list()is paginated and billed per call; do not scan a namespace on every request.- Bulk writes (up to 10,000 pairs) exist only on the REST API, not the binding.
Related: kv-cache-upstream-json-with-ttl-and-stale-fallback · wrangler-kv-r2-default-local-use-remote · durable-object-one-instance-per-id-with-sqlite