Configuration (14)

Last updated Oct 12, 2026 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.enabled flag in your Wrangler configuration, which turns caching on or off. You can override it per entrypoint and control cross-version behavior.
  • The Cache-Control (and cdn-cache-control, cloudflare-cdn-cache-control) headers your Worker sets on its responses, per RFC 9111 ↗︎.
  • The optional Cache-Tag response header for bulk purging or invalidation, and ctx.cache.purge() and ctx.cache.invalidate() to purge or invalidate from code.

That is the entire configuration surface.

Enable caching

Note

Requires Wrangler 4.69.0 or above.

Add a cache block to your Wrangler configuration:

{
    "name": "my-worker",
    "main": "src/index.ts",
    "upload_source_maps": true,
    // Set this to today's date
    "compatibility_date": "2026-10-27",
    "cache": {
        "enabled": true,
    },
}
name = "my-worker"
main = "src/index.ts"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-10-27"

[cache]
enabled = true

Setting 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.

Disable caching

To turn caching off, set cache.enabled to false (or remove the cache block) and redeploy:

{
    "name": "my-worker",
    "main": "src/index.ts",
    "upload_source_maps": true,
    // Set this to today's date
    "compatibility_date": "2026-10-27",
    "cache": {
        "enabled": false,
    },
}
name = "my-worker"
main = "src/index.ts"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-10-27"

[cache]
enabled = false

Disabling 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.

Per-entrypoint caching

Note

Requires Wrangler 4.107.0 or above.

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",
    "upload_source_maps": true,
    // Set this to today's date
    "compatibility_date": "2026-10-27",
    "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"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-10-27"

[cache]
enabled = true

[exports.default]
type = "worker"

[exports.default.cache]
  enabled = false

[exports.Admin]
type = "worker"

[exports.Admin.cache]
  enabled = true

Each 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.

For lowest latency, disable cache for an entrypoint instead of returning `Cache-Control: no-store` for all responses

Do not enable caching on a gateway entrypoint and then return Cache-Control: no-store on every response to keep it running. With caching enabled, each request to that entrypoint still consults the lower and upper cache tiers first — incurring the tiered-cache round trip — only to run the Worker and discard an uncacheable response. That adds latency for no benefit. Disable caching on the entrypoint in the exports map instead, so requests skip the cache lookup and go straight to your gateway logic.

Versioned deployments

The cache configuration is part of your Worker version:

  • Each version uploaded with wrangler deploy or wrangler versions upload captures whatever cache.enabled value is in its Wrangler configuration.
  • Rolling back to a previous version also rolls back the cache setting 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.

Cross-version caching

Note

The cross_version_cache option requires Wrangler 4.107.0 or above.

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",
    "upload_source_maps": true,
    // Set this to today's date
    "compatibility_date": "2026-10-27",
    "cache": {
        "enabled": true,
        "cross_version_cache": true,
    },
}
name = "my-worker"
main = "src/index.ts"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-10-27"

[cache]
enabled = true
cross_version_cache = true

Advanced 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.

Environment-specific configuration

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",
    "upload_source_maps": true,
    // Set this to today's date
    "compatibility_date": "2026-10-27",
    "cache": {
        "enabled": false,
    },
    "env": {
        "production": {
            "cache": {
                "enabled": true,
            },
        },
    },
}
name = "my-worker"
main = "src/index.ts"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-10-27"

[cache]
enabled = false

[env.production.cache]
enabled = true

Cache-Control semantics

With 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.

Responses with no `Cache-Control` header are still cached

If your Worker returns a response with no Cache-Control header (and no Expires header), Cloudflare applies RFC 9111 heuristic freshness ↗︎ with the per-status default TTLs in the following table. Responses whose status is not in the table are not cached by default.

Status code Description Default TTL
200 OK 7200 seconds (2 hours)
203 Non-Authoritative Information 7200 seconds (2 hours)
204 No Content 7200 seconds (2 hours)
300 Multiple Choices 1200 seconds (20 minutes)
301 Moved Permanently 1200 seconds (20 minutes)
404 Not Found 180 seconds (3 minutes)
405 Method Not Allowed 60 seconds (1 minute)
410 Gone 180 seconds (3 minutes)
414 URI Too Long 60 seconds (1 minute)
501 Not Implemented 60 seconds (1 minute)

For example, a 200 response that does not set Cache-Control is cached for 2 hours. A 404 is cached for 3 minutes.

If you want a response not to be cached, return Cache-Control: no-store (or private) explicitly. Omitting Cache-Control is not the same as opting out of the cache.

If you set Cache-Control (or Expires) on the response, these defaults do not apply — your directives are honored instead.

Cache Deception Armor

When a response falls back to heuristic freshness (no Cache-Control set), Workers Caching also runs Cache Deception Armor to defend against cache deception attacks ↗︎.

Cache Deception Armor only inspects responses whose Content-Type starts with text/ or application/ — the high-risk types for accidentally caching generated, user-specific content under a static-looking URL. For those responses, if the request URI has a file extension that maps to a known MIME type (for example, .css → text/css) and the actual Content-Type does not match, the response is not cached and Cf-Cache-Status is BYPASS. For example, a Worker that serves /style.css with Content-Type: text/html will hit this.

Responses with Content-Type outside text/* and application/* (for example, image/png, video/mp4) are not subject to the check — those types are rarely used to deliver private data, so the trade-off of a stricter check is not worth the false positives. A request URI with no file extension (for example, /api/users/42) also does not trigger the check.

Setting any explicit Cache-Control directive skips the check entirely — Cache Deception Armor only activates when freshness is being inferred heuristically.

Set the freshness window with max-age

Use max-age to control how long the response is treated as fresh:

src/index.jsjs

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>`;
}

src/index.tsts

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.

Use stale-while-revalidate for low-latency refreshes

When a cached response becomes stale, stale-while-revalidate lets Cloudflare return the stale response immediately and refresh it in the background:

src/index.jsjs

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",
            },
        });
    },
};

src/index.tsts

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;

`s-maxage`, `must-revalidate`, and `proxy-revalidate` disable `stale-while-revalidate`

If your response includes any of s-maxage, must-revalidate, or proxy-revalidate, the stale-serving behavior is disabled and Cloudflare will block on a fresh revalidation when the response expires. The same is true for stale-if-error. This follows RFC 9111 §4.2.4 ↗︎: those three directives forbid serving stale content.

When you want stale-while-revalidate to take effect at the edge, use max-age for the freshness window — not s-maxage. If you need a longer edge TTL than browsers should honor while still using stale-while-revalidate, use cdn-cache-control for the edge directive.

Choose TTL and stale-while-revalidate values

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.