---
title: Configuration
description: Enable and configure Workers Caching.
image: https://developers.cloudflare.com/dev-products-preview.png
---

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

[Skip to content](#%5Ftop) 

# 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](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/) — unless you [disable it for a specific entrypoint](#per-entrypoint-caching). Custom [RPC methods](https://developers.cloudflare.com/workers/runtime-apis/rpc/) 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](#per-entrypoint-caching) and control [cross-version behavior](#cross-version-caching).
* The `Cache-Control` (and `cdn-cache-control`, `cloudflare-cdn-cache-control`) headers your Worker sets on its responses, per [RFC 9111 ↗](https://www.rfc-editor.org/rfc/rfc9111).
* The optional `Cache-Tag` response header for bulk purging, and [ctx.cache.purge()](https://developers.cloudflare.com/workers/cache/purge/) for programmatic invalidation.

That is the entire configuration surface.

## Enable caching

Note

Requires Wrangler 4.69.0 or above.

Add a `cache` block to your Wrangler configuration:

* [  wrangler.jsonc ](#tab-panel-12158)
* [  wrangler.toml ](#tab-panel-12159)

**JSONC**

```jsonc
{
  "name": "my-worker",
  "main": "src/index.ts",
  // Set this to today's date
  "compatibility_date": "2026-07-15",
  "cache": {
    "enabled": true,
  },
}
```

**TOML**

```toml
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-07-15"


[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](#per-entrypoint-caching).

The `cache` block accepts two fields: `enabled` (required) and [cross\_version\_cache](#cross-version-caching) (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:

* [  wrangler.jsonc ](#tab-panel-12160)
* [  wrangler.toml ](#tab-panel-12161)

**JSONC**

```jsonc
{
  "name": "my-worker",
  "main": "src/index.ts",
  // Set this to today's date
  "compatibility_date": "2026-07-15",
  "cache": {
    "enabled": false,
  },
}
```

**TOML**

```toml
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-07-15"


[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](https://developers.cloudflare.com/workers/cache/purge/) 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](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-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:

* [  wrangler.jsonc ](#tab-panel-12164)
* [  wrangler.toml ](#tab-panel-12165)

**JSONC**

```jsonc
{
  "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 } },
  },
}
```

**TOML**

```toml
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-07-15"


[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](https://developers.cloudflare.com/workers/cache/examples/): 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](https://developers.cloudflare.com/workers/cache/#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](https://developers.cloudflare.com/workers/wrangler/commands/#deploy) or [wrangler versions upload](https://developers.cloudflare.com/workers/wrangler/commands/#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](https://developers.cloudflare.com/workers/versions-and-deployments/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).

## 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:

* [  wrangler.jsonc ](#tab-panel-12162)
* [  wrangler.toml ](#tab-panel-12163)

**JSONC**

```jsonc
{
  "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,
  },
}
```

**TOML**

```toml
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
```

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](https://developers.cloudflare.com/workers/cache/purge/) them, and during a [gradual deployment](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/) 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](https://developers.cloudflare.com/workers/cache/cache-keys/#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](https://developers.cloudflare.com/workers/wrangler/environments/). The typical pattern is to turn caching on in production once you are confident it is safe, while keeping staging uncached for easier debugging:

* [  wrangler.jsonc ](#tab-panel-12166)
* [  wrangler.toml ](#tab-panel-12167)

**JSONC**

```jsonc
{
  "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,
      },
    },
  },
}
```

**TOML**

```toml
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
```

## 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](https://developers.cloudflare.com/cache/concepts/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 ↗](https://www.rfc-editor.org/rfc/rfc9111#name-calculating-heuristic-fresh) 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](https://developers.cloudflare.com/cache/cache-security/cache-deception-armor/) to defend against [cache deception attacks ↗](https://owasp.org/www-community/attacks/Cache%5FPoisoning).

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:

* [  JavaScript ](#tab-panel-12170)
* [  TypeScript ](#tab-panel-12171)

**src/index.js**

```js
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.ts**

```ts
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](#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:

* [  JavaScript ](#tab-panel-12168)
* [  TypeScript ](#tab-panel-12169)

**src/index.js**

```js
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.ts**

```ts
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 ↗](https://www.rfc-editor.org/rfc/rfc9111#section-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](#header-precedence) 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.

Two common patterns:

* **Mostly static content with a small tolerance for staleness.** Use a short `max-age` (for example, 60 seconds) and a longer `stale-while-revalidate` window (for example, 3600 seconds). Most requests are `HIT`s; 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.

**TypeScript**

```ts
"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 ↗](https://www.rfc-editor.org/rfc/rfc9111#section-4.2.4) and applies to `stale-while-revalidate` as well.

### Header precedence

When multiple cache headers are present, the most specific wins:

1. `cloudflare-cdn-cache-control` — Cloudflare-specific, highest precedence. Consumed by Cloudflare and stripped from the response returned to clients.
2. `cdn-cache-control` — standard header for CDN-only directives. Respected by Cloudflare and passed through to downstream CDNs.
3. `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](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/) 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`:

* [  JavaScript ](#tab-panel-12174)
* [  TypeScript ](#tab-panel-12175)

**src/index.js**

```js
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.ts**

```ts
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](https://developers.cloudflare.com/cache/concepts/cache-control/) 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](https://developers.cloudflare.com/workers/cache/cache-keys/#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](https://developers.cloudflare.com/cache/concepts/cache-responses/).

### `Cache-Tag`

The [Cache-Tag](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/) 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.

* [  JavaScript ](#tab-panel-12172)
* [  TypeScript ](#tab-panel-12173)

**src/index.js**

```js
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.ts**

```ts
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](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/#a-few-things-to-remember) 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**. `Foo` and `foo` purge 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](https://developers.cloudflare.com/cache/concepts/cache-responses/#bypass). The most common triggers:

* The response includes a `Set-Cookie` header (unless `Cache-Control` includes `private="set-cookie"` or `no-cache="set-cookie"`, in which case the `Set-Cookie` is stripped from the cached copy).
* The request includes an `Authorization` header. The response is only stored if `Cache-Control` includes `public`, `must-revalidate`, or `s-maxage`, per [RFC 9111 §3.5 ↗](https://www.rfc-editor.org/rfc/rfc9111#name-storing-responses-to-authen).
* The response `Cache-Control` header includes `private` or `no-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-cache` alone** — every subsequent request is revalidated **inline** with your Worker before any bytes are served. If your Worker returns `304 Not Modified` for a conditional request, `Cf-Cache-Status` is `REVALIDATED` and the cached body is served. If your Worker returns a fresh `200`, `Cf-Cache-Status` is `EXPIRED` and 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-Status` is `UPDATING` for the duration of `N` seconds 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](https://developers.cloudflare.com/cache/concepts/cache-control/#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 ↗](https://www.rfc-editor.org/rfc/rfc9110.html#name-vary) and the cache-key calculation in [RFC 9111 ↗](https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-cache-keys-with). For an introduction with example code, refer to [Content negotiation with Vary](https://developers.cloudflare.com/workers/cache/#content-negotiation-with-vary).

How `Vary` is processed for Workers Caching:

* **All header names are honored.** Any header name your Worker lists in `Vary` participates 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, br` and `Accept-Encoding: br, gzip` produce 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 sets `Vary`.
* **`Vary: *` disables caching.** A wildcard variance cannot be satisfied deterministically from request headers, so the response is treated as uncacheable and `Cf-Cache-Status` is `BYPASS`.
* **Variants share a single purge identity.** [Purging](https://developers.cloudflare.com/workers/cache/purge/) by tag or path prefix invalidates every variant of a URL together. All variants must therefore use the same `Cache-Tag` values — 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 `Vary` on 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-Encoding` request 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 different `Content-Encoding` per request and set `Vary: Accept-Encoding`. Cloudflare stores one variant per distinct `Accept-Encoding` value 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 normalizing `Accept-Encoding` (for example, in a gateway Worker) before the response is generated.

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/cache/configuration/#page","headline":"Configuration · Cloudflare Workers docs","description":"Enable and configure Workers Caching.","url":"https://developers.cloudflare.com/workers/cache/configuration/","inLanguage":"en","image":"https://developers.cloudflare.com/dev-products-preview.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/workers/","name":"Workers"}},{"@type":"ListItem","position":3,"item":{"@id":"/workers/cache/","name":"Workers Cache"}},{"@type":"ListItem","position":4,"item":{"@id":"/workers/cache/configuration/","name":"Configuration"}}]}
```
