Cache key components
Workers Caching keys responses by: the target entrypoint (which named entrypoint of the Worker received the request; a default export and an exported class do not share cache even if identical); the path and query string of the request URL (query parameter order matters, and trailing slashes matter); the Worker version by default (each deployed version has its own cache, so new deployments start with a cold cache); and the invocation's ctx.props when invoked through service binding or RPC.
Cache key anti-cache-poisoning headers
The following request headers are included in the cache key as anti-cache-poisoning measures: x-http-method-override, x-http-method, x-method-override, x-forwarded-host, x-host, x-forwarded-scheme (unless its value is http or https), x-original-url, x-rewrite-url, and forwarded. The value of the Cloudflare-Workers-Version-Key header is also included; this header is not set by Cloudflare automatically and is only meaningful if a caller chooses to include it to explicitly partition the cache further.
What is not included in cache key
The cache key does not include: the HTTP method (GET and HEAD requests for the same URL share a single cache entry; HEAD on a cold cache is converted internally to GET, and a subsequent GET hits that entry; POST, PUT, PATCH, and DELETE are never cached); the request's host (the cache is keyed by path and query string, not the full URL); the request body (since only GET and HEAD are cacheable, but worth noting if your Worker reads request.body on a cacheable method).
Requests differing only in non-keyed headers share cache
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. If you need content negotiation, set Vary on the response or handle it inside your Worker to produce a canonical response per URL.
Cache belongs to Worker, not to domain
A Worker is a zoneless entity and can be invoked through multiple paths: directly on a workers.dev subdomain, through a route on any zone you control, through a custom domain (same Worker bound to many custom domains), or through a service binding from another Worker. Workers Caching treats all of these as the same Worker and uses a single shared cache. The cache key does not include the host, so a request to the same path hits the same cached entry whether it came through different domains or a workers.dev URL. This maximizes cache hit rate without losing correctness, since Worker responses are a function of code and inputs, not the domain.
Multi-tenant differentiation without cache key
If you 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 automatically. Instead, distinguish tenants at your gateway Worker and pass the tenant identifier via ctx.props, which is part of the cache key.
Default per-version cache behavior
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 without needing to purge; during a gradual deployment, old and new versions populate independent caches. The trade-off is that cache hit rate resets on every deployment as 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.
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 and responses logically belonging to one caller cannot leak to another through the cache.
Authentication not in ctx.props breaks cache isolation
If you authenticate callers through a mechanism other than ctx.props (for example, by reading a custom header your gateway Worker attaches), that input is not automatically part of the cache key. Two callers authenticated by different header values but otherwise identical requests will share a single cached entry, meaning one caller can receive another caller's response. The fix is to move per-caller authorization state into ctx.props. Your gateway Worker should populate ctx.props with whatever distinguishes callers before invoking the cached Worker.
Service binding URL is placeholder, path is cache key
When you call a service binding with fetch(), the hostname in the URL is a placeholder. The request is routed via the binding, not by DNS, so the hostname is never resolved and has no effect on caching. Only the path (and query string) contribute to the cache key, alongside the target entrypoint and ctx.props. If you want cached responses to differ for different callers, vary ctx.props. If you want them to differ by request, vary the path or query string. Varying the hostname does nothing.
Custom cache key with cf.cacheKey
When one entrypoint invokes another cached entrypoint through a ctx.exports loopback, the calling entrypoint can override the URL component of the cache key by setting cf.cacheKey on the request. A custom cache key replaces the path and query string in the cache key. Everything else described in cache key specification still applies: the target entrypoint and the caller's ctx.props remain part of the key, so a custom cache key cannot reach across entrypoints or across ctx.props, and multi-tenant isolation still holds even when callers choose their own keys. Two requests with different URLs but the same cf.cacheKey resolve to the same cache entry. Two requests with the same URL but different cf.cacheKey resolve to separate cache entries. Set cf.cacheKey to an empty string, or omit it, to fall back to the default URL-derived key.
Custom cache key use cases
Custom cache keys enable: ignoring parts of the URL (strip tracking parameters like utm_source or gclid, or drop a query string entirely, so variations that do not change the response share one cache entry); keying on something other than the URL (build the key from a value your gateway Worker trusts, like a normalized resource identifier, so several equivalent URLs map to one entry); partitioning the cache yourself (append a discriminating value like a content version to force separate entries for requests that would otherwise collide). For per-caller isolation, continue to use ctx.props rather than encoding caller identity into the cache key.
Custom cache key scope and account boundaries
cf.cacheKey is honored only when the call stays within your account. Cloudflare drops the cf object whenever a request crosses an account boundary (for example, a service binding to a Worker owned by a different account), so the custom key is disregarded and the cache key falls back to the request URL. A caller in one account can never influence or probe the cache of a Worker in another account. cf.cacheKey also has no effect on eyeball requests from browsers or API clients because the cf object on inbound requests is populated by Cloudflare, not by the client.
Example: Backend entrypoint with multi-tenant safety via ctx.props
```ts
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",
},
});
}
}
```
This example shows a cached Backend entrypoint that receives userId via ctx.props from a caller (like an auth gateway). Because ctx.props is part of the cache key, different users requesting the same URL get separate cache entries with no way for one user to see another's response.
Example: Gateway with custom cache key using cf.cacheKey
```ts
import { WorkerEntrypoint } from "cloudflare:workers";
// Cached entrypoint. Requests routed here through ctx.exports are served
// from cache when possible.
export class Backend extends WorkerEntrypoint<Env> {
async fetch(request: Request): Promise<Response> {
return new Response("Hello from the backend", {
headers: {
"Content-Type": "text/html",
"Cache-Control": "public, max-age=3600",
},
});
}
}
// Gateway entrypoint. Calls the cached Backend entrypoint via ctx.exports,
// which routes through the cache, and chooses the cache key for the call.
export default {
async fetch(request, env, ctx): Promise<Response> {
const url = new URL(request.url);
// Strip a tracking parameter so that requests differing only by
// `utm_source` resolve to the same cached entry.
url.searchParams.delete("utm_source");
return ctx.exports.Backend.fetch(request, {
cf: { cacheKey: url.pathname + url.search },
});
},
} satisfies ExportedHandler<Env>;
```
This example shows a gateway entrypoint calling a cached Backend entrypoint via ctx.exports and using cf.cacheKey to strip the utm_source tracking parameter, so requests differing only by that parameter resolve to the same cached entry.
cacheKey option in fetch cf object
The cacheKey option sets a custom cache key string that determines if two requests are the same for caching purposes. This feature is Enterprise-only. Use cf.cacheKey with a string value to make different URLs be treated as identical for caching. For example, cf.cacheKey: 'some-string' or cf.cacheKey: request.url.
Cache key limitations for cross-zone requests
Workers operating on behalf of different zones cannot affect each other's cache. You can only override cache keys when making requests within your own zone or requests to hosts that are not on Cloudflare. When making a request to another Cloudflare zone (belonging to a different Cloudflare customer), that zone fully controls how its own content is cached and you cannot override it.
Custom cache key example with load balancing
You can use custom cache keys to cache based on the original request URL while load-balancing between different origin servers. For example, randomly select between Amazon S3 and Google Cloud Storage URLs, but cache using the original request URL as the key with cf.cacheKey: request.url. This prevents duplicate cached copies of the same content from multiple origins.