Using the Cache API in Cloudflare Workers: Per-Datacenter Response Caching Done Right
How the Cloudflare Workers Cache API (caches.default) stores responses per datacenter, with a working example covering ctx.waitUntil, body cloning, cache-key normalization, and the mistakes that cause silent misses.
14 Jan 2026, 02:06 UTC

If your Worker fetches the same origin response repeatedly, you can cut both latency and origin load by caching that response at the edge with caches.default. The catch: the cache lives per datacenter, writes are asynchronous and easy to lose, and the cache key is the full request URL. This guide walks through a working setup, then the limits and mistakes that bite people in production.
How caches.default actually works
Every Cloudflare datacenter that runs your Worker exposes a Cache object as caches.default. The flow is simple: before calling fetch(), ask the cache for a stored response with cache.match(request). On a miss, fetch from the origin, then store a copy with cache.put(request, response). The next request for the same URL routed to the same datacenter gets the stored response without touching your origin.
Two properties define everything else about this API:
- It is per-datacenter, not global. A warm cache in Frankfurt does nothing for a request routed to Tokyo. Hit rates depend on how your traffic is distributed across Cloudflare's network.
- It is best-effort. Entries respect
Cache-Controlsemantics and can be evicted under memory pressure. A miss is always a normal outcome, so the cache can never be your source of truth.
A worked example
This Worker checks the cache first, fetches on a miss, and stores the response for 60 seconds. It also adds an X-Cache header so you can verify behavior from the outside:
export default {
async fetch(request, ctx) {
const cache = caches.default;
let res = await cache.match(request);
if (res) {
// Rebuild so we can add a header to the cached response
res = new Response(res.body, res);
res.headers.set('X-Cache', 'HIT');
return res;
}
res = await fetch(request);
// Only cache successful, cacheable responses
if (res.ok) {
const toCache = new Response(res.body, res);
toCache.headers.set('Cache-Control', 's-maxage=60');
ctx.waitUntil(cache.put(request, toCache));
}
const out = new Response(res.body, res);
out.headers.set('X-Cache', 'MISS');
return out;
},
};
Run this with wrangler dev locally (Wrangler's local runtime emulates the Cache API) or deploy it with wrangler deploy. No special permissions or bindings are needed — caches.default is available in every Worker.
Three details in this code matter:
ctx.waitUntil(...)aroundcache.put.putis asynchronous. If the Worker returns its response before the write completes, the runtime may cancel it — the single most common silent failure.waitUntilkeeps the invocation alive until the write finishes without delaying the client response.- The response is reconstructed before caching. A
Responsebody is a one-shot stream. If you hand the same body to bothcache.putand the client, one of them gets an empty body or you get an error about a disturbed/locked stream.new Response(res.body, res)(orres.clone()) gives each consumer its own copy. s-maxage=60is set explicitly. The Cache API honorsCache-Control. Setting it yourself makes the TTL deterministic instead of inheriting whatever the origin sent — or caching nothing because the origin saidno-store.
Cache keys: the full URL, and why that fragments your cache
The cache key is the entire request URL: protocol, host, path, and query string. ?utm_source=a and ?utm_source=b are two separate entries for identical content. If your hit rate is mysteriously low, key fragmentation is the usual suspect.
The fix is to normalize the key yourself by building a canonical request used only for cache operations:
const url = new URL(request.url);
url.search = ''; // drop tracking params, or sort/whitelist them
const cacheKey = new Request(url.toString(), request);
let res = await caches.default.match(cacheKey);
// ... on miss: ctx.waitUntil(caches.default.put(cacheKey, toCache));
Be deliberate here: if any query parameter genuinely changes the response (pagination, locale), it must stay in the key or you will serve the wrong content.
Limits and common mistakes
- Never cache personalized or credential-dependent responses under a shared key. Responses containing
Set-Cookie, or content that varies byAuthorizationor cookies, can leak across users if cached under a plain URL. Either skip caching for those requests or fold a safe discriminator (e.g., a hashed session segment) into the cache key. - Don't expect global consistency. If you need every user everywhere to see the same stored value, the Cache API is the wrong tool. Use KV, R2, or Durable Objects depending on your consistency and latency needs — and check their current limits and pricing before designing around them, since those change.
- Don't treat a miss as an error. Eviction under memory pressure is normal. Code that assumes a previously stored entry still exists will break intermittently and confusingly.
- Don't assume browser semantics. The Workers Cache API is modeled on the web Cache API but differs in details (for example,
cache.deleteandmatchbehaviors, and the fact that it ignores some request headers browsers consider). Test in the Workers runtime —wrangler devor Miniflare — not by reasoning from browser behavior. - Don't cache error responses unintentionally. The example guards with
res.ok; without that, a transient origin 500 can be served from cache for the full TTL.
Verifying it works
After deploying, request the same URL twice from one location:
curl -s -o /dev/null -D - https://your-worker.example.com/some-path
The first response should carry X-Cache: MISS, the second X-Cache: HIT, and the second should be measurably faster. For stronger evidence, check your origin's access logs (or add an origin-side counter header): the origin should be contacted at most once per cache window per datacenter. If you never see a HIT, check in order: is put wrapped in ctx.waitUntil? Is the origin sending Cache-Control: no-store that you didn't override? Are query strings fragmenting the key? Is your test traffic actually hitting the same colo twice?
If you see HITs locally with wrangler dev but not in production, that usually points to key fragmentation or cache-control issues rather than code — the local runtime is a single "datacenter," so per-colo behavior only shows up in real traffic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.