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 longer stale-while-revalidate window (for example, 3600 seconds). Most requests are HITs; 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:

  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 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. 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. 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 ↗︎.
  • 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 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 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 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.

Was this helpful?

YesNo

On this page

Configuration (14)·续 - 图1Docs

{"@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


Skip to content

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.