Configuration (14)·续
Two common patterns:
- Mostly static content with a small tolerance for staleness. Use a short
max-age(for example, 60 seconds) and a longerstale-while-revalidatewindow (for example, 3600 seconds). Most requests areHITs; occasional requests trigger a background refresh. - "Always serve from cache" for high-traffic endpoints. Use
max-age=0, stale-while-revalidate=<large>. Every request returns the previously cached response immediately and triggers a background refresh. Your Worker runs once per request to revalidate, so CPU costs are close to running the Worker every time. Freshness drops as request volume drops — if no request arrives for a long time, the next request will see stale content.
Serve stale on error with stale-if-error
stale-if-error lets Cloudflare return a previously cached response when the Worker fails while refreshing an expired cache entry — for example, when it throws, times out, or returns a 5xx response. This insulates clients from transient Worker failures.
"Cache-Control": "public, max-age=600, stale-if-error=86400",
When the Worker is producing a fresh response, stale-if-error has no effect. When the Worker fails while refreshing an expired entry, Cloudflare serves the last successful cached response (with Cf-Cache-Status: STALE) for up to the stale-if-error window. A true cache miss (no prior entry) cannot benefit from stale-if-error because there is nothing stale to serve — Worker errors flow through to clients in that case.
Note
If you do not set stale-if-error explicitly, and your response does not carry s-maxage, must-revalidate, or proxy-revalidate, Cloudflare's default behavior is to serve stale responses on Worker error indefinitely (as long as the cached entry has not been purged). This is helpful for resilience but can mask real failures from your monitoring. If you want Worker errors to surface to clients quickly, set stale-if-error=0.
If your response does carry s-maxage, must-revalidate, or proxy-revalidate, stale-if-error is disabled regardless of the directive's value or the default. Worker errors flow through to clients immediately. This follows RFC 9111 §4.2.4 ↗︎ and applies to stale-while-revalidate as well.
Header precedence
When multiple cache headers are present, the most specific wins:
cloudflare-cdn-cache-control— Cloudflare-specific, highest precedence. Consumed by Cloudflare and stripped from the response returned to clients.cdn-cache-control— standard header for CDN-only directives. Respected by Cloudflare and passed through to downstream CDNs.Cache-Control— standard HTTP header. Respected by Cloudflare and passed through to clients.
Use cloudflare-cdn-cache-control when you want a longer edge TTL than you expose to browsers without leaking the directive downstream.
Override Cache-Control from the calling Worker
Normally the callee decides how its responses are cached by setting Cache-Control on them. When one entrypoint invokes another cached entrypoint through a ctx.exports loopback, the calling entrypoint can instead supply the Cache-Control directive for that call by setting cf.cacheControl on the request.
Here the Backend entrypoint returns no Cache-Control of its own; the default entrypoint decides the caching policy when it calls Backend through ctx.exports:
src/index.jsjs
import { WorkerEntrypoint } from "cloudflare:workers";
// Cached entrypoint. It does not set Cache-Control itself.
export class Backend extends WorkerEntrypoint {
async fetch(request) {
return new Response("Hello from the backend", {
headers: { "Content-Type": "text/html" },
});
}
}
// Gateway entrypoint. Caches the Backend's response for this call for
// 5 minutes, without the Backend needing to set Cache-Control itself.
export default {
async fetch(request, env, ctx) {
return ctx.exports.Backend.fetch(request, {
cf: { cacheControl: "public, max-age=300" },
});
},
};
src/index.tsts
import { WorkerEntrypoint } from "cloudflare:workers";
// Cached entrypoint. It does not set Cache-Control itself.
export class Backend extends WorkerEntrypoint<Env> {
async fetch(request: Request): Promise<Response> {
return new Response("Hello from the backend", {
headers: { "Content-Type": "text/html" },
});
}
}
// Gateway entrypoint. Caches the Backend's response for this call for
// 5 minutes, without the Backend needing to set Cache-Control itself.
export default {
async fetch(request, env, ctx): Promise<Response> {
return ctx.exports.Backend.fetch(request, {
cf: { cacheControl: "public, max-age=300" },
});
},
} satisfies ExportedHandler<Env>;
Cloudflare treats cf.cacheControl as a trusted Cache-Control directive for caching the callee's response on that call. The value is a standard Cache-Control string and follows the same directive semantics described throughout this page — max-age, stale-while-revalidate, no-store, and so on. This lets a calling entrypoint decide how a cached entrypoint's responses are cached without modifying that entrypoint's code.
Like custom cache keys, cf.cacheControl is honored only for calls that stay within your account. Cloudflare drops the cf object whenever a request crosses an account boundary, so a caller in one account cannot change how a Worker in another account caches its responses. The directive also has no effect on eyeball requests, because the cf object on an inbound request is populated by Cloudflare rather than by the client.
Response headers
Cf-Cache-Status
Every response carries a Cf-Cache-Status header indicating what happened for that request. The values you will see most often are HIT, MISS, EXPIRED, REVALIDATED, UPDATING, STALE, and BYPASS. For the full set of values and their meanings, refer to Cloudflare cache responses.
Cache-Tag
The Cache-Tag response header attaches tags to a cached response so you can purge it later in bulk. Cloudflare consumes this header and strips it before the response reaches the client.
src/index.jsjs
export default {
async fetch(request) {
const html = `<!doctype html><title>Post</title>`;
return new Response(html, {
headers: {
"Content-Type": "text/html",
"Cache-Control": "public, max-age=3600",
"Cache-Tag": "blog,posts,post-123",
},
});
},
};
src/index.tsts
export default {
async fetch(request): Promise<Response> {
const html = `<!doctype html><title>Post</title>`;
return new Response(html, {
headers: {
"Content-Type": "text/html",
"Cache-Control": "public, max-age=3600",
"Cache-Tag": "blog,posts,post-123",
},
});
},
} satisfies ExportedHandler;
The Cache-Tag header value is a comma-separated list of tags. The same limits as the zone cache apply — refer to Cache tag limits for the full list. The most common constraints to keep in mind:
- Tag values must be printable ASCII (
0x21–0x7E) — no spaces, no Unicode, no control characters. - Each tag is at most 1024 characters long.
- A response can carry up to 1000 tags for purge purposes.
- Tag matching at purge time is case-insensitive.
Fooandfoopurge the same set of responses.
Invalid tags (over-length, containing spaces, or containing non-ASCII characters) are silently dropped during cache storage — the response is still cached with the remaining valid tags, but you have no way to detect which tags were dropped. Validate tags in your Worker before returning them if this matters.
Automatic bypass conditions
Workers Caching inherits Cloudflare's standard cache bypass rules. The most common triggers:
- The response includes a
Set-Cookieheader (unlessCache-Controlincludesprivate="set-cookie"orno-cache="set-cookie", in which case theSet-Cookieis stripped from the cached copy). - The request includes an
Authorizationheader. The response is only stored ifCache-Controlincludespublic,must-revalidate, ors-maxage, per RFC 9111 §3.5 ↗︎. - The response
Cache-Controlheader includesprivateorno-store.
When any of these apply, Cf-Cache-Status is BYPASS and your Worker runs on every request.
`no-cache` is not a bypass
Cache-Control: no-cache does not bypass the cache. The response is stored, but the cached entry is treated as stale immediately — so the next request always needs to consult your Worker before serving. The exact behavior depends on what other directives accompany no-cache:
no-cachealone — every subsequent request is revalidated inline with your Worker before any bytes are served. If your Worker returns304 Not Modifiedfor a conditional request,Cf-Cache-StatusisREVALIDATEDand the cached body is served. If your Worker returns a fresh200,Cf-Cache-StatusisEXPIREDand the cached body is replaced. Either way, the Worker runs and CPU time is billed.no-cache, stale-while-revalidate=N— Cloudflare serves the cached body immediately and refreshes it in the background by invoking your Worker.Cf-Cache-StatusisUPDATINGfor the duration ofNseconds after the entry was last refreshed. After the SWR window elapses, behavior reverts to inline revalidation.
In both cases, your Worker is invoked on every request, so CPU time is billed each time. The cached body is preserved across revalidations.
Refer to Understand no-store and no-cache directives for the full distinction between these directives.
Status codes that are never cached
A few status codes are never stored, even with explicit Cache-Control directives:
520–526(Cloudflare failsafe responses) are treated as transient errors and never cached.
Range requests
Workers Caching serves Range requests from a cached full response — your Worker does not have to implement byte-range slicing.
When a client sends a Range request, Cloudflare strips the Range header before invoking your Worker and asks your Worker for the full body. Your Worker returns a normal 200 response with a Cache-Control header (as it would for any other request), Cloudflare stores that full response, and then slices out the requested byte range and returns it to the client as a 206 Partial Content response (or 416 Range Not Satisfiable if the range is invalid). Subsequent Range requests to the same URL are satisfied entirely from the cached entry — your Worker is not invoked, and Cf-Cache-Status is HIT.
For example, a GET with Range: bytes=0-9 against a cold cache produces a MISS on the way in (your Worker runs and returns the full body), then returns 206 with the first 10 bytes. A follow-up GET Range: bytes=10-19 for the same URL is a HIT and returns those 10 bytes from cache without invoking your Worker.
If your Worker returns a 206 response of its own — for example, because you implemented Range handling inside the Worker — Cloudflare treats it as an uncacheable response and it is not stored. Return a full 200 and let Workers Caching handle range slicing.
Vary
When your Worker returns a Vary response header, Cloudflare stores a separate cached variant per distinct combination of the listed request header values, and only returns a variant whose stored values match the incoming request. This implements RFC 9110 ↗︎ and the cache-key calculation in RFC 9111 ↗︎. For an introduction with example code, refer to Content negotiation with Vary.
How Vary is processed for Workers Caching:
- All header names are honored. Any header name your Worker lists in
Varyparticipates in the variant key. There is no allowlist. - Values are compared verbatim. Cloudflare does not normalize the listed request headers before keying.
Accept-Encoding: gzip, brandAccept-Encoding: br, gzipproduce two separate variants even though they are semantically identical. If you need to fold equivalent values onto the same variant, normalize the headers your Worker sees in a gateway Worker before passing the request on, or canonicalize them inside the Worker that setsVary. Vary: *disables caching. A wildcard variance cannot be satisfied deterministically from request headers, so the response is treated as uncacheable andCf-Cache-StatusisBYPASS.- Variants share a single purge identity. Purging by tag or path prefix invalidates every variant of a URL together. All variants must therefore use the same
Cache-Tagvalues — assigning different tags to different variants results in inconsistent purges. - Image transformation features take precedence. Responses produced by Polish or Image Resizing already generate their own variants, and
Varyon those responses is ignored.
Accept-Encoding and Content-Encoding
Your Worker controls its own content negotiation. Whatever Content-Encoding your Worker sets on the response is what Cloudflare stores and serves to subsequent requests.
If your Worker needs to return different encodings to different clients, you have two options:
- Pick one canonical encoding inside your Worker. Decide based on the
Accept-Encodingrequest header, encode the body once, and return a single representation. Subsequent requests for that URL hit the same cached entry regardless of what they accept. This produces the highest cache hit rate but requires you to decide which clients you serve which encoding to. - Vary on
Accept-Encoding. Return a differentContent-Encodingper request and setVary: Accept-Encoding. Cloudflare stores one variant per distinctAccept-Encodingvalue the Worker has seen. Because comparison is verbatim, clients that send semantically equivalent values in different orders or with different quality factors produce separate variants — keep cache fan-out under control by normalizingAccept-Encoding(for example, in a gateway Worker) before the response is generated.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/cache/configuration/#page","headline":"Configuration","description":"Enable and configure Workers Caching.","url":"https://developers.cloudflare.com/workers/cache/configuration/","inLanguage":"en","image":"https://developers.cloudflare.com/workers/cache/configuration/og.png?v=757cd9bbad5852dd","dateModified":"2026-10-12","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Diagnose why Workers Caching is not behaving the way you expect. title: Debugging image: https://developers.cloudflare.com/workers/cache/debugging/og.png?v=323bb85e9cced0d1
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt
Use this file to discover all available pages before exploring further.
Docs