Cache keys (2)

Last updated Oct 12, 2026 Every cached response is stored under a cache key. When a request arrives, Cloudflare computes a cache key for it and looks it up — on a hit, the stored response is returned; on a miss, your Worker runs and its response is stored under that key for next time.

Two requests that produce the same cache key share the same cached response. Two requests that produce different cache keys get independent cached entries.

This page explains what Workers Caching puts into the cache key, why each component is there, and how to reason about it when designing your Worker.

What goes into the cache key

Workers Caching keys responses by:

  • The target entrypoint — which specific named entrypoint of the Worker received the request. A default export and an exported class are different entrypoints and do not share a cache even if they produce identical responses.
  • The path and query string of the request URL. Query parameter order matters — ?a=1&b=2 and ?b=2&a=1 are different cache keys. Trailing slashes matter too.
  • The Worker version, by default. Each deployed version has its own cache, so a new deployment does not serve responses written by a previous version. You can turn this off with cache.cross_version_cache to share cached responses across versions. See Invalidating cache across deployments.
  • The invocation's ctx.props, when the Worker is invoked through a service binding or RPC. See Multi-tenant safety with ctx.props.

As anti-cache-poisoning measures, the key also includes:

  • The x-http-method-override, x-http-method, and x-method-override request headers.
  • The x-forwarded-host, x-host, x-forwarded-scheme (unless its value is http or https), x-original-url, x-rewrite-url, and forwarded request headers.
  • The value of the Cloudflare-Workers-Version-Key request header. This header is not set by Cloudflare automatically — it is only meaningful if a caller (for example, an upstream Worker or proxy) chooses to include it to explicitly partition the cache further. This is independent of the automatic per-version keying described above, which is controlled by cache.cross_version_cache.

These three bullets are not something you should normally need to reason about. Some frameworks interpret the method-override and URL-rewrite headers as overriding the effective method or URL of a request, which can lead to cache poisoning ↗︎ if two requests differ only in those headers but produce materially different responses. Including them in the cache key ensures a poisoned entry only affects requests that carry the same poisoned header.

Requests that differ only in request headers that are not part of the cache key (for example, User-Agent, Accept-Language, Cookie, or Authorization) return the same cached response. This is usually what you want — you do not want every user agent string or language preference producing a separate cache entry. If you do need content negotiation, set Vary on the response, or handle it inside your Worker and produce a canonical response per URL.

Notably, the cache key does not include:

  • The HTTP method. GET and HEAD requests for the same URL share a single cache entry. A HEAD request can be served from a GET fill (Cloudflare returns the cached headers without the body). In the other direction, a HEAD request on a cold cache is converted to a GET internally so the full asset is fetched and stored — a subsequent GET then hits the entry that HEAD populated. ( POST, PUT, PATCH, and DELETE are never cached at all, so the question does not arise for them.)
  • The request's host. The Worker's cache is keyed by path and query string, not the full URL. See The cache belongs to the Worker, not to a domain.
  • The request body. Since only GET and HEAD are cacheable, this is rarely relevant — but worth noting if your Worker reads request.body on a cacheable method, the body does not partition the cache.

At launch, you cannot inspect the exact cache key Cloudflare computed for a request. The primary signals you have for understanding cache behavior are the Cf-Cache-Status response header and per-invocation cache-hit information in the Workers observability dashboard. See Inspecting the cache key.

The cache belongs to the Worker, not to a domain

A Worker is a zoneless entity. It can be invoked through several different paths:

  • Directly on a workers.dev subdomain.
  • Through a route on any zone you control.
  • Through a custom domain — and you can bind the same Worker to many custom domains.
  • Through a service binding from another Worker, with an arbitrary placeholder hostname in the URL.

Workers Caching treats all of these as the same Worker and uses a single shared cache across them. The cache key does not include the host, so a request to /api/users/42 hits the same cached entry whether it came in through api.example.com, api.example.net, a service binding, or a workers.dev URL.

This is the behavior you almost always want. A Worker's responses are a function of its code and its inputs, not of which domain the request arrived through — so caching them once and serving that response back to every ingress path maximizes the cache hit rate without losing correctness.

If you genuinely need different cached responses for the same path on different hostnames — for example, white-labeled tenants where tenant-a.example.com/index and tenant-b.example.com/index must produce different content — the cache key does not do this for you automatically. Instead, distinguish the tenants at your gateway Worker and pass the tenant identifier via ctx.props, which is part of the cache key.

Invalidating cache across deployments

By default, the currently invoked Worker version is part of the cache key. Each deployed version has its own cache, so:

  • A new deployment starts from a cold cache and never serves responses that a previous version wrote.
  • Cache-affecting changes apply immediately when the new version goes live — you do not have to purge anything to stop serving old content.
  • During a gradual deployment, the old and new versions populate independent caches, so a slice of traffic on the new version never receives the old version's responses.

This is the default because it is the simplest behavior to reason about. The trade-off is that cache hit rate resets on every deployment — the first requests to a new version are misses while its cache fills. This is the most common reason a Worker's cache hit rate drops right after a deploy.

Share the cache across versions

If you deploy frequently and your responses rarely change between deployments, throwing away a warm cache on every deploy is wasteful. Set cache.cross_version_cache to true to drop the version from the cache key and share cached responses across versions. A response written by version A is then still served after version B is deployed, as long as its TTL has not expired.

This maximizes cache hit rate at the expense of slower rollouts: because a deployment no longer invalidates the cache, a change that alters response content will not take effect for already-cached entries until they expire or you purge them. When you have cross_version_cache enabled and need a deployment to take effect immediately, use one of the two tools below.

Tag responses by version, purge the tag on rollback

If you want fine-grained control, tag each cached response with the Worker version that produced it. Later, purging that version tag removes every entry that version wrote, without affecting cached responses from other versions.

This uses the version metadata binding to read the current version ID at request time, and prepends it as a Cache-Tag value. See Version-specific purging for the full pattern with code.

This is the best option if you have enabled cross_version_cache and might need to roll back a specific version without blowing away cached content from working versions.

Purge everything after deploy

The simpler approach: after each deploy, hit a small Worker endpoint from your CI that calls ctx.cache.purge({ purgeEverything: true }). The next request after the purge re-populates the cache from whichever Worker version is live at that moment.

This is coarser but requires zero in-Worker logic. Use it if you have enabled cross_version_cache but still want specific deployments to invalidate the cache. With the default per-version cache, deployments already start from a cold cache, so this is unnecessary.

Multi-tenant safety with ctx.props

When your Worker is invoked through a service binding or RPC, the caller's ctx.props is part of the cache key. Two callers that invoke your Worker with different ctx.props get separate cached entries — one caller can never receive another caller's cached response.

This is the mechanism that makes caching safe for multi-tenant Workers invoked over a service binding. If you use ctx.props to carry per-caller authorization context — user ID, tenant ID, organization, role — caching is safe by default. Responses that logically belong to one caller cannot leak to another through the cache.

src/backend.jsjs

import { WorkerEntrypoint } from "cloudflare:workers";

export default class Backend extends WorkerEntrypoint {
    async fetch(request) {
        // ctx.props.userId is set by the caller (for example, an auth gateway).
        // Because it is part of the cache key, User A and User B requesting the
        // same URL get separate cache entries — there is no way for one to
        // see the other's response.
        const { userId } = this.ctx.props;
        const data = { userId, timestamp: Date.now() };

return new Response(JSON.stringify(data), {
            headers: {
                "Content-Type": "application/json",
                "Cache-Control": "public, max-age=300",
            },
        });
    }
}

src/backend.tsts

import { WorkerEntrypoint } from "cloudflare:workers";

interface Props {
    userId: string;
}

export default class Backend extends WorkerEntrypoint<Env, Props> {
    async fetch(request: Request): Promise<Response> {
        // ctx.props.userId is set by the caller (for example, an auth gateway).
        // Because it is part of the cache key, User A and User B requesting the
        // same URL get separate cache entries — there is no way for one to
        // see the other's response.
        const { userId } = this.ctx.props;
        const data = { userId, timestamp: Date.now() };

return new Response(JSON.stringify(data), {
            headers: {
                "Content-Type": "application/json",
                "Cache-Control": "public, max-age=300",
            },
        });
    }
}