Configuration
Workers Caching is configured per Worker, in your Wrangler configuration file. When enabled, caching applies to every fetch() invocation — eyeball requests, service binding fetch() calls, and loopback fetch() calls between entrypoints via ctx.exports — unless you disable it for a specific entrypoint. Custom RPC methods bypass the cache.
This is your Worker's cache — configured through your Worker's code and Wrangler file. Your Worker controls its cache entirely through:
- The
cache.enabledflag in your Wrangler configuration, which turns caching on or off. You can override it per entrypoint and control cross-version behavior. - The
Cache-Control(andcdn-cache-control,cloudflare-cdn-cache-control) headers your Worker sets on its responses, per RFC 9111 ↗. - The optional
Cache-Tagresponse header for bulk purging, andctx.cache.purge()for programmatic invalidation.
That is the entire configuration surface.
Add a cache block to your Wrangler configuration:
{ "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-07-15", "cache": { "enabled": true, },}name = "my-worker"main = "src/index.ts"# Set this to today's datecompatibility_date = "2026-07-15"
[cache]enabled = trueSetting cache.enabled to true causes Cloudflare to check the cache before invoking your Worker on every HTTP request. This is the default for every entrypoint; you can override it per entrypoint with exports.
The cache block accepts two fields: enabled (required) and cross_version_cache (optional). Any other fields are reserved for future use and may cause validation errors in future versions of Wrangler.
To turn caching off, set cache.enabled to false (or remove the cache block) and redeploy:
{ "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-07-15", "cache": { "enabled": false, },}name = "my-worker"main = "src/index.ts"# Set this to today's datecompatibility_date = "2026-07-15"
[cache]enabled = falseDisabling caching does not purge previously cached responses — it only stops Cloudflare from consulting or populating the cache on subsequent requests. If you re-enable caching later, any entries that are still within their TTL become usable again. If you need cached responses to stop being served immediately, purge the cache after disabling.
cache.enabled sets the default for the whole Worker, but a Worker can expose several entrypoints — the default export and any number of named WorkerEntrypoint classes — and you can turn caching on or off for each one independently. Use the exports map, keyed by entrypoint name, with "default" referring to the default export:
{ "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-07-15", "cache": { "enabled": true, }, "exports": { // Opt the default entrypoint out of caching. "default": { "type": "worker", "cache": { "enabled": false } }, // Keep caching on for the Admin entrypoint. "Admin": { "type": "worker", "cache": { "enabled": true } }, },}name = "my-worker"main = "src/index.ts"# Set this to today's datecompatibility_date = "2026-07-15"
[cache]enabled = true
[exports.default]type = "worker"
[exports.default.cache] enabled = false
[exports.Admin]type = "worker"
[exports.Admin.cache] enabled = trueEach entry is { "type": "worker", "cache": { "enabled": <boolean> } }. A per-entrypoint cache.enabled overrides the top-level cache.enabled for that entrypoint; entrypoints you do not list inherit the top-level value. You can also enable caching for a single entrypoint without a top-level cache block by listing only that entrypoint.
This lets you opt specific entrypoints in and out without changing your Worker code:
- Opt an entrypoint out to keep it running on every request — the natural fit for a gateway or router entrypoint that authenticates, normalizes, or dispatches, and should never itself be served from cache. This is the recommended way to build the gateway pattern: disable caching on the gateway entrypoint and enable it on the inner entrypoint the gateway calls through
ctx.exports. - Opt an entrypoint in to cache only the specific entrypoints that return reusable responses, leaving the rest of the Worker uncached.
The cache configuration is part of your Worker version:
- Each version uploaded with
wrangler deployorwrangler versions uploadcaptures whatevercache.enabledvalue is in its Wrangler configuration. - Rolling back to a previous version also rolls back the
cachesetting attached to that version. - You can use gradual deployments to turn caching on for a percentage of traffic before applying it to 100%. During a gradual rollout from a version with caching disabled to a version with caching enabled, traffic routed to the old version runs uncached as it did before, and traffic routed to the new version consults and populates the cache. By default, the Worker version is part of the cache key, so the two versions populate independent cache entries and do not serve each other's responses — see Cross-version caching.
By default, the Worker version is part of the cache key. Each deployed version has its own isolated cache, so a new deployment starts from an empty cache and never serves responses written by a previous version. This is the default because it is the simplest behavior to reason about: a new deployment applies immediately, and you never serve a response that a superseded version produced.
The trade-off is that cache hit rate resets on every deployment. Because a new version cannot reuse the previous version's cached responses, the first requests after a deploy are misses while the new version's cache fills. This is the most common reason a Worker's cache hit rate drops right after a deployment.
If you want to maximize cache hit rate and are willing to accept slower rollouts of cache-affecting changes, set cross_version_cache to true. Cached responses are then shared across versions — a response written by one version can be served by a later version as long as its TTL has not expired:
{ "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-07-15", "cache": { "enabled": true, "cross_version_cache": true, },}name = "my-worker"main = "src/index.ts"# Set this to today's datecompatibility_date = "2026-07-15"
[cache]enabled = truecross_version_cache = trueAdvanced users who deploy frequently and whose responses do not change between most deployments should consider enabling cross_version_cache — it avoids throwing away a warm cache on every deploy. The cost is that a deployment no longer invalidates the cache: after a change that alters response content, older cached responses continue to be served until they expire or you purge them, and during a gradual deployment both versions share one cache. When you need a deployment to take effect immediately with cross_version_cache enabled, purge the cache after deploying, or tag responses by version — see Invalidating cache across deployments.
cross_version_cache only has an effect when caching is enabled. It applies to every entrypoint whose cache is on.
The cache block can be set at the top level and overridden per environment. The typical pattern is to turn caching on in production once you are confident it is safe, while keeping staging uncached for easier debugging:
{ "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-07-15", "cache": { "enabled": false, }, "env": { "production": { "cache": { "enabled": true, }, }, },}name = "my-worker"main = "src/index.ts"# Set this to today's datecompatibility_date = "2026-07-15"
[cache]enabled = false
[env.production.cache]enabled = trueWith caching enabled, your Worker is the origin for Cloudflare's cache. Standard HTTP Cache-Control directives on the response your Worker returns determine whether and for how long Cloudflare caches it. For the full list of directives and how they interact, refer to Cache-Control.
Use max-age to control how long the response is treated as fresh:
export default { async fetch(request) { const body = await renderPage(request);
return new Response(body, { headers: { "Content-Type": "text/html", // Cached for 1 hour at Cloudflare's edge and in the browser. "Cache-Control": "public, max-age=3600", }, }); },};
// Replace with your own rendering logic.async function renderPage(request) { return `<!doctype html><title>Home</title><h1>Hello</h1>`;}export default { async fetch(request): Promise<Response> { const body = await renderPage(request);
return new Response(body, { headers: { "Content-Type": "text/html", // Cached for 1 hour at Cloudflare's edge and in the browser. "Cache-Control": "public, max-age=3600", }, }); },} satisfies ExportedHandler;
// Replace with your own rendering logic.async function renderPage(request: Request): Promise<string> { return `<!doctype html><title>Home</title><h1>Hello</h1>`;}If you need browsers and the edge to cache for different durations, use cdn-cache-control (or cloudflare-cdn-cache-control) for the edge-only directive and keep Cache-Control for what browsers see. Refer to Header precedence below.
When a cached response becomes stale, stale-while-revalidate lets Cloudflare return the stale response immediately and refresh it in the background:
export default { async fetch(request) { const data = { timestamp: Date.now() };
return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", // Fresh for 10 minutes; may be served stale for up to 1 minute // while a background revalidation runs. "Cache-Control": "public, max-age=600, stale-while-revalidate=60", }, }); },};export default { async fetch(request): Promise<Response> { const data = { timestamp: Date.now() };
return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", // Fresh for 10 minutes; may be served stale for up to 1 minute // while a background revalidation runs. "Cache-Control": "public, max-age=600, stale-while-revalidate=60", }, }); },} satisfies ExportedHandler;High cache hit rate and high freshness are in tension. Background revalidation hides the latency of refreshing the cache, but your Worker still runs once per revalidation — it is not free.
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.
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.
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.
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:
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" }, }); },};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.
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.
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.
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", }, }); },};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.
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.
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.
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.
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.
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.