pickuma.
Infrastructure

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.

6 min read

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() with cacheTtl: 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:

  1. The availability check reads slug:acme, gets null, and that null is now cached in the location serving that user.
  2. The write succeeds.
  3. The confirmation page — same user, same location, seconds later — reads slug:acme, hits the cached null, 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.

Try opencode

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?
Not reliably. If that key has a cached copy in the location handling the request, the read returns the old value until the cache entry expires, which can be up to 60 seconds. Keep the written value in memory and return that instead of re-reading it.
Can I force a fresh read from Workers KV?
There is no bypass. cacheTtl has a documented minimum of 60 seconds and no zero value. The only dependable way to read something with no cache entry is to read a key that has never been read, so version the key name instead of overwriting the same key.
Is the 60-second window a guarantee?
No. It is a design expectation stated in the documentation, not an SLA commitment, and it says nothing about the tail. Cloudflare has had incidents where KV was disrupted for hours. Anything that must stay correct needs defined behaviour for a stale or failed read rather than a timeout assumption.

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

See all Infrastructure articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.