Workers Cache

Last updated Oct 12, 2026 Workers Cache lets Cloudflare return cached HTTP responses from your Worker without executing your Worker code. When an incoming request matches a cached response, Cloudflare serves the response directly from its edge cache — reducing latency and Workers CPU usage.

Caching works for any fetch() invocation of the Worker — eyeball requests (requests from browsers and API clients), requests sent through service bindings, and loopback fetch() calls between entrypoints via ctx.exports. You control caching with standard HTTP Cache-Control directives on your responses.

Your Worker's cache

Workers Cache is your Worker's cache. It is owned by your Worker, operated by your Worker, and private to your Worker.

A Worker is a zoneless entity — a Worker can be bound to any number of zones, run on workers.dev, or be invoked entirely through service bindings without ever touching a zone. The cache follows the Worker, not a zone, so:

  • No zone configuration for caching applies to Workers Caching. Cache Rules, Cache Response Rules, Page Rules, cache level settings, the zone's default cached-file-extensions list, and every other zone-level cache control have no effect on a Worker's cache.
  • Your Worker is in full control. You set Cache-Control headers on your responses, and Cloudflare honors them per RFC 9111 ↗︎. That is the entire configuration surface.
  • The cache is shared across every way the Worker can be invoked. A Worker bound to api.example.com, api.example.net, and invoked over a service binding serves the same cached responses to all three — the cache is keyed by the request path, entrypoint, ctx.props, and (by default) the Worker version, not by hostname. See Cache keys.

The Worker is the configuration surface

A Worker is already infinitely customizable. You can change response bodies, rewrite headers, branch on any request attribute, call out to other Workers via service bindings or ctx.exports, and compose logic across an entire system.

Workers Caching leans on that. Instead of introducing a separate configuration layer for caching behavior, it lets your Worker express that intent directly — through the Cache-Control headers it returns, the ctx.props it accepts, and the programmatic purges it issues. Anything you might want to configure about caching, you can configure in code:

  • Want a longer TTL for certain paths? Branch on the path in your Worker and set a different max-age.
  • Want to strip a tracking query parameter before caching? Rewrite the URL or ctx.props in a gateway Worker before dispatching.
  • Want per-tenant cache partitioning? Set the tenant identifier in ctx.props — that is in the cache key.
  • Want to bypass the cache for authenticated users? Return Cache-Control: private, or rely on the automatic bypass triggered by Set-Cookie and Authorization.

The Worker you already wrote is the configuration mechanism. Workers Caching runs in front of it and honors whatever headers the Worker returns.

When caching helps

Caching is a good fit for Workers that:

  • Perform CPU-intensive work whose result can be reused across requests — content generation, template rendering, data transformation.
  • Fetch data from a slow origin or third-party API and want to absorb that latency for subsequent requests.
  • Power a server-rendered or statically generated site where many requests produce identical responses.

Caching is not useful for per-user responses that change on every request, non-idempotent operations (POST, PUT, DELETE), or responses that must be computed fresh every time.

How it works

With caching enabled, Cloudflare checks the cache before running your Worker. On a hit, the cached response is returned directly. On a miss, your Worker runs, and if the response is cacheable per its Cache-Control header, Cloudflare stores it for the next request.

flowchart LR
    accTitle: Cache before a Worker request flow
    accDescr: Request arrives at Cloudflare, cache is consulted before Worker execution.

Request["Request"] --> Cache{"Cache"}
    Cache -- Hit --> Response["Cached response returned"]
    Cache -- Miss --> Worker["Worker runs"]
    Worker --> Store["Response stored in cache"]
    Store --> Response2["Response returned"]

Tiered cache

Workers Caching is tiered by default. Cloudflare operates two layers of cache for your Worker:

  • Lower tier — a cache in the Cloudflare data center closest to the eyeball. Every data center that receives traffic for your Worker has its own lower-tier cache.
  • Upper tier — a smaller set of data centers that every lower tier consults on a miss. The upper tier aggregates cache fills across the whole network.

A request is served from the lower tier if it is a hit there. If it is a miss, the lower tier asks the upper tier. If the upper tier also misses, your Worker finally runs to generate the response — and that response is stored in both tiers on the way back out, so subsequent requests from any data center benefit.

flowchart LR
    accTitle: Tiered cache for Workers
    accDescr: A request hits the lower-tier cache first, then the upper-tier cache, then the Worker.

Request["Request"] --> Lower{"Lower-tier cache<br/>(near eyeball)"}
    Lower -- Hit --> Response["Cached response returned"]
    Lower -- Miss --> Upper{"Upper-tier cache"}
    Upper -- Hit --> Lower
    Upper -- Miss --> Worker["Worker runs"]
    Worker --> Upper

This is the same topology that powers Tiered Cache for zones, applied automatically to your Worker. You do not configure it, and the tiering runs regardless of whether your Worker uses Smart Placement.

Why this matters: the first request for a given cache key anywhere on Earth populates the upper tier. Every later request, from any Cloudflare data center, can be served from the upper tier without running your Worker — even if the lower tier at that location has never seen the request before. Cache hit ratios are substantially higher than a single flat cache layer.

Request collapsing

When many requests for the same cache key arrive simultaneously at a Cloudflare data center and the response is not yet cached, Cloudflare runs your Worker once and serves the resulting response to every waiting request. This is the same request collapsing mechanism the zone cache uses, applied automatically to Workers Caching. The waiting requests block on a per-cache-key cache lock until the first request produces a response.

flowchart LR
    accTitle: Cache request collapsing for Workers
    accDescr: Many simultaneous requests for the same cache key produce one Worker invocation; all requests receive the same response.

R1["Request 1"] --> Lock
    R2["Request 2"] --> Lock
    R3["Request 3"] --> Lock
    Rn["..."] --> Lock
    Lock{"Cache lock<br/>(per cache key, per data center)"}
    Lock -- "first request" --> Worker["Worker runs once"]
    Worker --> Response["Response<br/>streamed to all<br/>waiting requests"]

Why this matters: without request collapsing, a sudden burst of traffic to a fresh URL would invoke your Worker once per request, multiplying CPU billing and load on any backend the Worker calls. With request collapsing, that burst still produces one Worker invocation.

A few details to keep in mind:

  • Collapsing is per cache key, per data center. Requests that produce different cache keys do not collapse with each other. Two data centers that both miss simultaneously each run your Worker once (the upper tier consolidates further; refer to Tiered cache).
  • Streaming responses are collapsed too. Waiting requests are joined to the in-flight response stream so they receive the body as it is produced — they do not have to wait for the full response before any bytes are returned.
  • Collapsing does not apply to uncacheable responses. If the Worker's response is uncacheable ( BYPASS, DYNAMIC), each request gets its own invocation. The cache only collapses requests that produce a response the cache is allowed to store.

This is one of the most significant differences between Workers Caching and the Cache API — the Cache API does not collapse concurrent requests, so a burst of traffic to a fresh URL invokes your Worker once per request.

Quickstart

This quickstart walks you through enabling caching, deploying, and observing the cache in action.

1. Enable caching in 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

2. Return a cacheable response from your Worker

Use max-age to control how long Cloudflare caches each response:

src/index.jsjs

export default {
    async fetch(request) {
        const body = JSON.stringify({
            timestamp: new Date().toISOString(),
            random: Math.random(),
        });

return new Response(body, {
            headers: {
                "Content-Type": "application/json",
                // Cache for 1 hour; serve stale for up to 5 minutes while revalidating.
                "Cache-Control": "public, max-age=3600, stale-while-revalidate=300",
            },
        });
    },
};

src/index.tsts

export default {
    async fetch(request): Promise<Response> {
        const body = JSON.stringify({
            timestamp: new Date().toISOString(),
            random: Math.random(),
        });

return new Response(body, {
            headers: {
                "Content-Type": "application/json",
                // Cache for 1 hour; serve stale for up to 5 minutes while revalidating.
                "Cache-Control": "public, max-age=3600, stale-while-revalidate=300",
            },
        });
    },
} satisfies ExportedHandler;

3. Deploy and observe the cache

Deploy your Worker:

npx wrangler deploy

Then send two requests and look at the Cf-Cache-Status response header:

curl -I https://my-worker.example.workers.dev/

First request — expectedtxt

HTTP/2 200
cache-control: public, max-age=3600, stale-while-revalidate=300
cf-cache-status: MISS
curl -I https://my-worker.example.workers.dev/

Second request — expectedtxt

HTTP/2 200
cache-control: public, max-age=3600, stale-while-revalidate=300
cf-cache-status: HIT

The second request receives the cached response. The timestamp and random values in the body are identical between the two requests, even though the Worker generates fresh ones on every run — confirming that the second request did not execute your Worker.

What gets cached

  • HTTP invocations of the Worker's fetch handler are eligible for caching, including eyeball requests, service binding fetch() calls, and loopback fetch() calls via ctx.exports.
  • Only GET and HEAD requests are cached. Other methods always invoke your Worker. GET and HEAD for the same URL share a single cache entry — see Cache keys.
  • Only fetch() invocations go through the cache. Custom RPC methods on a WorkerEntrypoint (for example ctx.exports.Backend.getUser(id)) bypass the cache entirely and always run the callee. To cache a piece of work, expose it as a fetch handler on its own entrypoint.
  • WebSocket upgrade requests bypass the cache. A GET request carrying Upgrade: websocket always invokes your Worker.
  • Other invocation types — scheduled (Cron Triggers), queue consumers, Workflows, Tail Workers, Durable Object invocations, Email Workers — always run without cache involvement.
  • Cacheability is determined by the response headers your Worker returns. Workers Caching follows the semantics defined in RFC 9111 ↗︎, including heuristic freshness ↗︎ for responses that do not carry Cache-Control. Refer to Cache-Control for the full list of directives Cloudflare respects.
  • Cloudflare's standard cache bypass conditions apply. In particular, responses with a Set-Cookie header and requests with an Authorization header trigger automatic bypass.
  • Previews are supported. Each Preview caches independently of your production deployment, so testing a cache-affecting change in a Preview never touches production's cached responses.
  • Workers for Platforms is supported. Each user Worker has its own cache, isolated from the dispatcher and from other user Workers in the namespace.

The Cf-Cache-Status response header tells you what happened for each request. The values you will see most often are HIT, MISS, EXPIRED, REVALIDATED, UPDATING, STALE, and BYPASS. Refer to Cloudflare cache responses for the full set of values.

Content negotiation with Vary

Workers Caching honors the Vary ↗︎ response header as defined in RFC 9110 ↗︎ and RFC 9111 ↗︎. When your Worker returns a Vary header, Cloudflare stores a separate cached variant per distinct combination of the listed request header values, and only returns a cached variant when the incoming request's headers match the ones the variant was stored under.

This lets a single URL cache multiple representations — for example, different encodings, different content types, or different languages — without your Worker coordinating content negotiation by hand: