Cache keys (2)·续
Caution
If you authenticate callers through some 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, which means 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. Cloudflare's standard automatic bypass rules (Set-Cookie, Authorization) can also prevent the problem by disabling caching entirely for authenticated requests, but ctx.props is the recommended approach because it still lets you cache.
Service binding URL
Service binding calls deserve a specific note because the URL you pass does not mean what you might think it means.
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 — the hostname is never resolved. And because the host is not part of the cache key (as described in The cache belongs to the Worker, not to a domain), the placeholder has no effect on caching either. Only the path (and query string) contribute to the cache key, alongside the target entrypoint and ctx.props:
src/gateway.jsjs
export default {
async fetch(request, env, ctx) {
// "internal" here is just a placeholder — it is not routed anywhere
// and is not part of the cache key.
//
// What identifies this cached response is:
// - the BACKEND entrypoint
// - the path "/api/users/42"
// - whatever ctx.props the gateway passes along
return env.BACKEND.fetch("http://internal/api/users/42");
},
};
src/gateway.tsts
interface Env {
BACKEND: Fetcher;
}
export default {
async fetch(request, env, ctx): Promise<Response> {
// "internal" here is just a placeholder — it is not routed anywhere
// and is not part of the cache key.
//
// What identifies this cached response is:
// - the BACKEND entrypoint
// - the path "/api/users/42"
// - whatever ctx.props the gateway passes along
return env.BACKEND.fetch("http://internal/api/users/42");
},
} satisfies ExportedHandler<Env>;
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.
Inspecting the cache key
At launch, two signals give you visibility into cache behavior:
- The
Cf-Cache-Statusresponse header. The values you will see most often areHIT,MISS,EXPIRED,REVALIDATED,UPDATING,STALE, andBYPASS.HITmeans Cloudflare returned a cached response without running your Worker.MISSmeans your Worker ran and the response was stored.UPDATINGmeans the cached response was stale and your Worker ran in the background to refresh it.BYPASSmeans caching was disabled for this request. Refer to Cloudflare cache responses for the full set of values. - Cache hits in the Workers observability dashboard. Each invocation surfaces whether it was served from cache, so you can filter and aggregate cache-hit behavior across your Worker's traffic.
Cloudflare does not currently expose the cache key composition itself. If two requests you expected to share a cached response do not, you have to reason about what part of the key differed from the components listed in What goes into the cache key. For a walkthrough of common caching problems and how to diagnose them, refer to Debugging.
Custom cache keys
By default, the path and query string of the request URL form the URL component of the cache key. When one entrypoint invokes another cached entrypoint through a ctx.exports loopback, the calling entrypoint can override that component by setting cf.cacheKey on the request.
In the example below, the Backend entrypoint is the cached one. The default entrypoint forwards requests to it through ctx.exports, choosing the cache key itself:
src/index.jsjs
import { WorkerEntrypoint } from "cloudflare:workers";
// Cached entrypoint. Requests routed here through ctx.exports are served
// from cache when possible.
export class Backend extends WorkerEntrypoint {
async fetch(request) {
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) {
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 },
});
},
};
src/index.tsts
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>;
A custom cache key replaces the path and query string in the cache key. Everything else described in What goes into the cache key still applies:
- The target entrypoint and the caller's
ctx.propsremain part of the key. A custom cache key cannot reach across entrypoints or acrossctx.props, so the multi-tenant isolation described above still holds even when callers choose their own keys. A custom key only ever addresses entries within the callee's own cache namespace. - Two requests with different URLs but the same
cf.cacheKeyresolve to the same cache entry. This is how you collapse several URLs onto a single cached response. - Two requests with the same URL but different
cf.cacheKeyresolve to separate cache entries.
Set cf.cacheKey to an empty string, or omit it, to fall back to the default URL-derived key.
In this pattern the default entrypoint is a gateway that should run on every request, so disable caching on it and leave it on for Backend (see Per-entrypoint caching):
{
"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 },
"exports": {
"default": { "type": "worker", "cache": { "enabled": false } },
"Backend": { "type": "worker", "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
[exports.default]
type = "worker"
[exports.default.cache]
enabled = false
[exports.Backend]
type = "worker"
[exports.Backend.cache]
enabled = true
What you can do with a custom cache key
- Ignore parts of the URL. Strip tracking parameters (
utm_source,gclid), or drop a query string entirely, so that variations that do not change the response share one cache entry. - Key on something other than the URL. Build the key from a value your gateway Worker trusts — for example, a normalized resource identifier — so that several equivalent URLs map to one entry.
- Partition the cache yourself. Append a discriminating value to the key (for example, 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 — ctx.props is part of the key automatically and cannot be bypassed.
Custom keys apply to same-account calls only
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. When that happens, the custom key is disregarded and the cache key falls back to the request URL, so a caller in one account can never influence (or probe) the cache of a Worker in another account.
This also means cf.cacheKey has no effect on eyeball requests. The cf object on an inbound request from a browser or API client is populated by Cloudflare, not by the client, so a client cannot set its own cache key.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/cache/cache-keys/#page","headline":"Cache keys","description":"How Workers Caching builds cache keys, with guidance for service bindings, multi-tenant Workers, and gradual deployments.","url":"https://developers.cloudflare.com/workers/cache/cache-keys/","inLanguage":"en","image":"https://developers.cloudflare.com/workers/cache/cache-keys/og.png?v=be4ec8a6bbecd55f","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: Enable and configure Workers Caching. title: Configuration image: https://developers.cloudflare.com/workers/cache/configuration/og.png?v=757cd9bbad5852dd
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.
Docs