Debugging
If caching is not behaving the way you expect, the Cf-Cache-Status response header is the first place to look. Every response carries it, and its value tells you exactly what happened for that request.
Send two requests to the same URL and compare the headers:
curl -I https://my-worker.example.workers.dev/api/users/42curl -I https://my-worker.example.workers.dev/api/users/42Match the status value against the scenarios below.
Cf-Cache-Status is not present. Check that your wrangler version is 4.69.0 or above, and that
wrangler.toml or wrangler.jsonc has cache.enabled = true for that worker.
Cf-Cache-Status is MISS on every request, or DYNAMIC, or BYPASS. Caching is not storing anything, or a bypass rule is firing.
Check your Cache-Control header. The response must carry directives that make it cacheable:
public, max-age=N— cached in Cloudflare and browsers forNseconds.
A response with Cache-Control: private or no-store is not stored, and Cf-Cache-Status is BYPASS.
A response with Cache-Control: no-cache is stored, but Cloudflare treats every subsequent request as stale and consults your Worker before serving. The exact Cf-Cache-Status depends on whether stale-while-revalidate is also set:
- With just
Cache-Control: no-cache, every subsequent request triggers an inline revalidation.Cf-Cache-StatusisREVALIDATEDif your Worker returns304 Not Modified(body served from cache), orEXPIREDif your Worker returns a fresh200(body replaced). - With
Cache-Control: no-cache, stale-while-revalidate=N, the cached body is served immediately and the Worker runs in the background.Cf-Cache-StatusisUPDATINGfor the SWR window.
If you wanted long-lived cache hits, use max-age instead. Refer to no-cache is not a bypass.
If the response carries no Cache-Control header at all, behavior depends on the status code: Workers Caching applies RFC 9111 heuristic freshness ↗ and caches default-cacheable status codes for a heuristic TTL — for example, 200 is cached for 2 hours and 404 for 3 minutes. For the full table of default TTLs, refer to Responses with no Cache-Control header are still cached in the configuration reference. If you do not want any of these defaults to apply, set Cache-Control explicitly on the response.
Check the request method. Only GET and HEAD requests are cached. Everything else is BYPASS. GET and HEAD requests for the same URL share the same cache entry — refer to Cache keys for how Cloudflare handles populating the cache from either method.
Check for automatic bypass conditions. Cloudflare bypasses the cache when:
- The response includes a
Set-Cookieheader. - The request includes an
Authorizationheader, unless the response explicitly setsCache-Control: public,must-revalidate, ors-maxage.
If your Worker unconditionally sets Set-Cookie (for example, a session cookie on every response), the response is never cached. Either remove the cookie from cacheable responses, or separate cookie-setting and cacheable responses into different routes.
Check the status code. Workers Caching follows RFC 9111 ↗. Responses with status codes that are not cacheable by default (for example, 401, 403, 500) are not stored unless you explicitly mark them with cacheable directives.
A few status codes are never cached, even with explicit Cache-Control:
520–526are treated as Cloudflare failsafe responses and are never stored.206 Partial Contentreturned by your Worker is not stored. Workers Caching handlesRangerequests itself by fetching the full body from your Worker and slicing from the cached entry — if your Worker returns its own206, that response is treated as uncacheable. Return a full200instead. Refer toRangerequests.
Cf-Cache-Status is MISS on the first request but still MISS on subsequent requests.
The cache is likely partitioned. The cache key includes the request path, the target entrypoint, and the invocation's ctx.props. Two requests that look the same to you may produce different cache keys if any of these differ.
Common causes:
- The URL path or query string differs between requests (even trailing slashes matter).
- The calling Worker passes different
ctx.propsfor each request — for example, a different user ID. - Requests are hitting different named entrypoints of the same Worker.
Cloudflare does not currently expose the cache key composition, so you cannot see the computed key directly. Instead, walk through the components listed in Cache keys and verify each one is the same for both requests.
This is expected with the default configuration. By default, the Worker version is part of the cache key, so each new version starts from a cold cache and cannot reuse the previous version's cached responses. The first requests after a deploy are misses while the new version's cache fills, then the hit rate recovers.
If you deploy frequently and your responses rarely change between deployments, enable cache.cross_version_cache to share cached responses across versions and avoid resetting the cache on every deploy. The trade-off is that cache-affecting changes no longer apply immediately — see below.
By default a deployment takes effect immediately, because the Worker version is part of the cache key and the new version starts from a cold cache. If you are still seeing responses from a previous version, you have cache.cross_version_cache enabled, which shares cached entries across versions. To force a deployment to take effect while keeping cross_version_cache on:
- Call
ctx.cache.purge({ purgeEverything: true })after deploying. This is the simplest approach. - Tag each cached response with the producing version using the version metadata binding, then purge that tag on rollback. Refer to Version-specific purging.
If your origin data changed but requests still return stale content:
- Check the TTL. The response stays cached for
max-ageseconds. You may be looking at a response that is still within its freshness window. - Purge the affected responses. Use
ctx.cache.purge()with tags or a path prefix to invalidate specific entries. Refer to Purging the cache. - Add tags at write time. If you did not set
Cache-Tagheaders, you cannot purge by tag. Add tags to your cached responses, deploy, and once new entries are written they become purgeable.
This should not happen if you use ctx.props for per-caller authorization context. If it does, one of the following is true:
- You are authenticating callers with a header or query parameter that is not part of the cache key. Move the authorization input into
ctx.props. Refer to Multi-tenant safety withctx.props. - You are calling a service binding with a user-specific query parameter that is not present. The query string is part of the cache key; make sure each caller's request path actually differs.
UPDATING means the response was served from cache while stale and your Worker is running in the background to refresh it. This is expected behavior when using stale-while-revalidate.
If you see UPDATING more often than you expect:
- Your
max-ageis shorter than your request arrival rate. Every request that arrives aftermax-ageelapses triggers a revalidation. - With
max-age=0, stale-while-revalidate=<large>, every request triggers a revalidation. This is "always serve from cache" behavior, not "don't run the Worker." Refer to Choose TTL and stale-while-revalidate values.
UPDATING is emitted only when all of the following are true:
- A cached entry exists and is past its freshness window (stale).
- The response carries
stale-while-revalidate=N, and the request arrives withinNseconds of the entry going stale. - The response does not also carry
s-maxage,must-revalidate, orproxy-revalidate.
If any of those is false, requests for stale entries fall through to inline revalidation instead, producing EXPIRED (Worker returned a fresh body) or REVALIDATED (Worker returned 304 Not Modified).
Common reasons UPDATING does not appear:
- No
stale-while-revalidatedirective on the response. The default SWR window is0, so without an explicit directive every stale request is foreground-revalidated. s-maxage,must-revalidate, orproxy-revalidateis present. Per RFC 9111 §4.2.4 ↗, these directives forbid serving stale content, so Cloudflare disablesstale-while-revalidate(andstale-if-error) when any of them is present. Usemax-agefor the edge freshness window if you want stale-serving to work.- The SWR window has elapsed. If your response uses
max-age=60, stale-while-revalidate=120, you will seeUPDATINGfor requests arriving in the 120 seconds after the entry goes stale. Requests arriving after that revert to inline revalidation.
STALE means Cloudflare served a previously cached response because your Worker errored on the request that would have refreshed it — for example, the Worker threw, timed out, or returned a 5xx response. This is stale-if-error behavior. Refer to Serve stale on error with stale-if-error.
If you see STALE and did not expect it:
- Your Worker is failing on cache fills or revalidations. Check the Workers observability dashboard for errors on the requests that should have produced a fresh response. The fact that clients see a stale response instead of a
5xxis masking a real failure. - You did not set
stale-if-errorexplicitly, and your response does not includes-maxage/must-revalidate/proxy-revalidate. In that case, Cloudflare's default behavior is to serve stale responses on Worker error indefinitely, as long as the cached entry has not been purged. If you want errors to surface to clients quickly, setstale-if-error=0inCache-Control. For details, refer to Serve stale on error withstale-if-error. - A previously deployed version is being served. If you deployed a fix but
STALEkeeps appearing, the cached entry from the broken version is still being served on every error. Purge the affected entries to force a fresh fill from the current version.
To distinguish a STALE from a normal HIT in client-side observability, log Cf-Cache-Status alongside the response — STALE is the only signal that the Worker is failing and clients are not seeing it.
If a response is too large to cache, Cloudflare does not store it. You will see Cf-Cache-Status: MISS on every request even though the response otherwise looks cacheable.
For per-plan response size limits, refer to Cacheable size limits. Note that at launch all Workers Caching responses are subject to the Free plan size limit — refer to Response size for details.
At launch, the primary debugging surfaces are the Cf-Cache-Status response header and per-invocation cache-hit information in the Workers observability dashboard.