Workers KV's 60-Second Consistency Window: What It Actually Costs You
Workers KV's documented 60-second window is a cache TTL you cannot lower, not a replication delay. Where it bites: cached nulls, uniqueness checks, multi-key updates — and when a Durable Object is the right swap.
Cloudflare’s Workers KV documentation gives you a number: a write may take up to 60 seconds to become visible in other locations. That number usually gets filed away as a worst-case replication delay and forgotten. It is more useful to read it as a cache TTL you are not permitted to lower — the cacheTtl option on get() defaults to 60 seconds and has a documented minimum of 60 seconds — and the cost it imposes on your application depends on something the docs do not put front and centre: how recently the key you just wrote was read in the location doing the reading.
Three published limits frame everything below.
- Propagation of a write to other locations: documented as up to 60 seconds.
get()withcacheTtl: default 60 seconds, minimum 60 seconds. There is no zero.- Writes to a single key: roughly one per second. KV is not a counter, and it is not a lock.
We did not run a global propagation benchmark for this article. A table of PoP-by-PoP timings measured from one machine on one afternoon would look like evidence and be worth very little — the number you actually care about is a distribution that moves with routing and cache occupancy. What follows is about the shape of the failure, which is stable, rather than the milliseconds, which are not.
The 60 seconds is a cache TTL, not a replication delay
There are two mechanisms sitting between your put() and someone else’s get(), and they fail differently.
The first is propagation from KV’s central store outward. The second is a read cache in front of it, local to the location serving the request. When a key is requested in a location that holds no cached copy, the read falls through toward the central store, and you often observe the new value well inside the 60-second window. When a key does have a cached copy in that location, you get the cached copy until its TTL expires, no matter how quickly the underlying propagation finished.
That inverts the intuition most caches train into you. Here, staleness scales with popularity. A key nobody reads is close to fresh on first access. A key read a thousand times a minute in Frankfurt is pinned to whatever Frankfurt last fetched. The keys carrying the highest staleness risk are exactly the ones you reached for KV to hold: feature flags, routing tables, config blobs, session lookups.
Two consequences follow directly.
Your staging environment lies to you. Low traffic means cold caches, which means reads-after-write that look fast and correct. The behaviour that bites you only appears under the read volume that keeps the cache warm, and that is production.
Per-key TTL also means per-key expiry. If a logical change spans two keys, they expire independently. A reader in one location can see the new value of key A and the old value of key B for tens of seconds. KV offers no cross-key atomicity and no transactions, and nothing in the API will warn you that you just wrote a change that cannot land atomically.
Treat the 60 as a design guideline from the docs rather than a guarantee. It is not an SLA figure, and under incident conditions the real window is unbounded — Cloudflare’s published post-mortem for the 12 June 2025 outage describes a multi-hour KV disruption that propagated into other Cloudflare products built on KV. A design whose correctness rests on “it will be there in a minute” has no defined behaviour for the day it is not.
The failure that reads like data loss: a cached null
The pattern that generates support tickets is a uniqueness check written the obvious way:
const taken = await env.KV.get(`slug:${slug}`);
if (taken) return new Response('already taken', { status: 409 });
await env.KV.put(`slug:${slug}`, userId);
This has two defects, and the second is the expensive one.
The first is the familiar race: two concurrent requests both read null, both write, the later write wins, and the loser is never told. No error surfaces anywhere. You find it weeks later in a support thread.
The second is that a miss is cacheable. Cloudflare’s KV documentation describes negative lookups as cached the same way hits are — the answer “this key does not exist” is itself an entry with a TTL. So the single-user, zero-concurrency path breaks too:
- The availability check reads
slug:acme, getsnull, and thatnullis now cached in the location serving that user. - The write succeeds.
- The confirmation page — same user, same location, seconds later — reads
slug:acme, hits the cachednull, and renders a not-found state.
The user watched the form succeed and then watched their thing fail to exist. That reads as data loss, and it self-heals in about a minute, which makes it close to impossible to reproduce on demand. Worth verifying negative-cache behaviour against the current docs before you build around it: the KV caching layer was rearchitected in 2024 and the details are not frozen.
Two fixes, in order of how much they buy you.
Do not re-read what you just wrote. After put(), return the value you already hold in memory. This sounds too obvious to write down until you notice how many frameworks POST, redirect, and then re-fetch — at which point the read is a fresh request that knows nothing about the write and goes straight to the local cache.
Make keys immutable and version the pointer. Write the payload under a content-addressed or versioned key such as config:v41, never overwritten, then update one small pointer key. A stale read then returns a coherent older version rather than a mixture. The window does not disappear; the failure mode changes from inconsistent to behind, and behind is something you can reason about, display, and alert on.
If that migration means touching every KV call site in a codebase, it is grep-able, mechanical work — the kind worth handing to an agent under one clear rule (no get() on a key this request writes) rather than doing by hand across forty files.
opencode
Open-source terminal coding agent, useful for rule-driven mechanical sweeps like rewriting every read-after-write KV call site in a repo.
Free and open source; you pay your model provider directly.
Affiliate link · We earn a commission at no cost to you.
What we would reach for instead, and the condition that flips it
KV stays the right default when reads vastly outnumber writes, values are whole documents fetched by key, and a stale answer costs nothing worse than a slightly old page. Published paid-plan rates are $0.50 per million reads and $5.00 per million writes, deletes and lists, with a free tier of 100,000 reads and 1,000 writes per day. Read-heavy workloads are cheap here in a way strongly consistent stores are not, and that is the actual reason to accept the window.
The condition that flips it is narrow and absolute: correctness depends on a read reflecting your own write, or two keys must change together. Then use a Durable Object. One object per entity gives you single-threaded, strongly consistent access, and the price is a network hop to that object’s home region — a read KV would serve locally in single-digit milliseconds can become a cross-continent round trip. For a global read path that is a real regression, which is why the usual answer is both: a Durable Object or D1 as the system of record, KV as the read-optimised projection in front of it, and a version pointer so you can measure how far behind the projection is.
If you cannot currently say which of your KV keys are read-after-write critical, that inventory is the work to do before the next incident rather than after it.
FAQ
If I write a key and read it later in the same Worker request, do I get the new value?
Can I force a fresh read from Workers KV?
Is the 60-second window a guarantee?
Tools used in this review
Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.
Related reading
2026-08-13
Cache Races in a Publish Pipeline: Why Your IndexNow Ping Misses the Pages You Just Shipped
A deploy API returning 200 does not mean your new URLs are reachable at the edge. Here is how the deploy-then-ping race silently burns IndexNow submissions, and the verification sequence that stops it.
2026-08-12
Object Storage Lifecycle Policies: Cutting Storage and Egress Cost Without Losing Data You Need
How to audit an S3-compatible bucket, write lifecycle rules that actually pay for themselves, and avoid the transition fees, minimum-duration charges, and versioning traps that make bills go up instead of down.
2026-08-13
When There Is No API: Driving Chrome With the DevTools Protocol, and When Not To
CDP gives a scheduled agent a real browser when a site ships no API. Here is what it costs in memory and wall clock, the four failure modes that only surface on a cron, and the cases where you should not do it at all.
2026-08-12
Running a Small Kubernetes Cluster in 2026: k3s vs Talos vs a Managed Control Plane
k3s, Talos Linux, and a managed control plane solve different halves of the same problem. What each one actually costs you in money, upgrade work, and 2am debugging on a three-node cluster.
2026-08-12
Postgres Connection Pooling in 2026: PgBouncer vs Supavisor vs Driver-Side Pools
Postgres forks a process per connection, so pooling is not optional past a few dozen clients. How PgBouncer, Supavisor, and driver-side pools differ, and what transaction mode breaks.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.