每个缓存的响应都存储在一个 cache key(缓存键) 下。当请求到达时,Cloudflare 会为其计算缓存键并查找——命中时返回已存储的响应;未命中时运行你的 Worker,并将其响应存储在该键下,供下次使用。
产生相同缓存键的两个请求共享同一份缓存响应。产生不同缓存键的两个请求则拥有独立的缓存条目。
本页说明 Workers Caching 在缓存键中包含哪些内容、每项存在的原因,以及在设计 Worker 时如何据此推理。
Workers Caching 按以下因素为响应建立键:
- 目标 entrypoint——接收请求的 Worker 的哪个具体命名 entrypoint。
default导出与导出的 class 是不同的 entrypoint,即使它们产生完全相同的响应,也不会共享缓存。 - 请求 URL 的路径和查询字符串。查询参数的顺序很重要——
?a=1&b=2与?b=2&a=1是不同的缓存键。尾部斜杠同样重要。 - 默认情况下,Worker 版本。每个已部署版本拥有独立缓存,因此新部署不会提供由旧版本写入的响应。你可以通过
cache.cross_version_cache关闭此行为,在版本间共享缓存响应。参见跨部署使缓存失效。 - 当 Worker 通过 service binding 或 RPC 调用时,调用中的
ctx.props。参见使用ctx.props实现多租户安全。
作为防缓存投毒措施,键还包含:
x-http-method-override、x-http-method和x-method-override请求头。x-forwarded-host、x-host、x-forwarded-scheme(除非其值为http或https)、x-original-url、x-rewrite-url和forwarded请求头。Cloudflare-Workers-Version-Key请求头的值。Cloudflare 不会自动设置此请求头——只有当调用方(例如上游 Worker 或代理)选择包含它时才有意义,用于进一步显式划分缓存。这与上文描述的自动按版本键控无关,后者由cache.cross_version_cache控制。
这三条通常不需要你特别考虑。某些框架会将 method-override 和 URL-rewrite 请求头解释为覆盖请求的有效方法或 URL,若两个请求仅在这些请求头上不同却产生实质不同的响应,可能导致缓存投毒 ↗。将它们纳入缓存键可确保被投毒的条目只影响携带相同投毒请求头的请求。
仅在不属于缓存键的请求头(例如 User-Agent、Accept-Language、Cookie 或 Authorization)上不同的请求会返回相同的缓存响应。这通常正是你想要的——你不希望每个 user agent 字符串或语言偏好都产生单独的缓存条目。若确实需要内容协商,请在响应上设置 Vary,或在 Worker 内部处理并针对每个 URL 产生规范响应。
值得注意的是,缓存键不包含:
- HTTP 方法。 同一 URL 的
GET和HEAD请求共享单个缓存条目。HEAD请求可由GET填充提供(Cloudflare 返回缓存的响应头而不含 body)。反之,冷缓存上的HEAD请求会在内部转换为GET,以便获取并存储完整资源——随后的GET即可命中由HEAD填充的条目。(POST、PUT、PATCH和DELETE完全不会被缓存,因此不存在此问题。) - 请求的 host。 Worker 的缓存按路径和查询字符串键控,而非完整 URL。参见缓存属于 Worker,而非 domain。
- 请求 body。 由于只有
GET和HEAD可缓存,这很少相关——但若你的 Worker 在可缓存方法上读取request.body,body 不会划分缓存。
在发布时,你无法查看 Cloudflare 为请求计算的确切缓存键。理解缓存行为的主要信号是 Cf-Cache-Status 响应头,以及 Workers 可观测性仪表板中每次调用的缓存命中信息。参见检查缓存键。
Worker 是无 zone 的实体。它可以通过多种不同路径被调用:
- 直接在
workers.dev子域上。 - 通过你控制的任意 zone 上的路由(route)。
- 通过自定义域(custom domain)——且可将同一 Worker 绑定到多个自定义域。
- 通过来自另一个 Worker 的 service binding,URL 中使用任意占位 hostname。
Workers Caching 将以上所有路径视为同一 Worker,并在它们之间使用单一共享缓存。缓存键不包含 host,因此无论请求来自 api.example.com、api.example.net、service binding 还是 workers.dev URL,/api/users/42 都会命中同一缓存条目。
这几乎总是你想要的行为。Worker 的响应是其代码与输入的函数,而非请求从哪个 domain 进入——因此缓存一次并将该响应提供给所有入口路径,可在不损失正确性的前提下最大化缓存命中率。
若你确实需要同一 path 在不同 hostname 上产生不同的缓存响应——例如白标租户场景,tenant-a.example.com/index 与 tenant-b.example.com/index 必须产生不同内容——缓存键不会自动为你实现这一点。相反,应在 gateway Worker 中区分租户,并通过 ctx.props 传递租户标识符,而 ctx.props 是 缓存键的一部分。
默认情况下,当前调用的 Worker 版本是缓存键的一部分。每个已部署版本拥有独立缓存,因此:
- 新部署从冷缓存开始,永远不会提供旧版本写入的响应。
- 影响缓存的更改在新版本上线时立即生效——无需 purge 即可停止提供旧内容。
- 在渐进式部署(gradual deployment)期间,新旧版本填充独立缓存,因此新版本上的流量切片永远不会收到旧版本的响应。
这是默认行为,因为它最容易推理。代价是每次部署都会重置缓存命中率——新版本的前几个请求都是未命中,直到其缓存被填充。这是 Worker 缓存命中率在部署后立即下降的最常见原因。
若你频繁部署且响应在部署之间很少变化,每次部署都丢弃温缓存是浪费。将 cache.cross_version_cache 设为 true,从缓存键中移除版本并在版本间共享缓存响应。只要 TTL 未过期,版本 A 写入的响应在部署版本 B 后仍会被提供。
这以较慢的上线速度为代价最大化缓存命中率:由于部署不再使缓存失效,改变响应内容的更改对已缓存条目不会生效,直到它们过期或你 purge 它们。启用 cross_version_cache 且需要部署立即生效时,使用以下两种工具之一。
若需要细粒度控制,为每个缓存响应标记产生它的 Worker 版本。之后 purge 该版本 tag 会移除该版本写入的每个条目,而不影响其他版本的缓存响应。
这使用版本元数据绑定(binding)在请求时读取当前版本 ID,并将其作为 Cache-Tag 值前置。完整模式与代码参见按版本 purge。
若已启用 cross_version_cache 且可能需要回滚特定版本而不清除正常工作版本的缓存内容,这是最佳选择。
更简单的方法:每次部署后,从 CI 调用一个小型 Worker 端点,执行 ctx.cache.purge({ purgeEverything: true })。purge 后的下一个请求会从当时上线的 Worker 版本重新填充缓存。
这更粗粒度,但无需 Worker 内逻辑。若已启用 cross_version_cache 但仍希望特定部署使缓存失效,可使用此方法。使用默认的按版本缓存时,部署已从冷缓存开始,因此无需此操作。
当 Worker 通过 service binding 或 RPC 调用时,调用方的 ctx.props 是缓存键的一部分。以不同 ctx.props 调用 Worker 的两个调用方获得独立的缓存条目——一个调用方永远不会收到另一个调用方的缓存响应。
这是使通过 service binding 调用的多租户 Worker 缓存安全可用的机制。若使用 ctx.props 携带按调用方划分的授权上下文——用户 ID、租户 ID、组织、角色——缓存默认是安全的。逻辑上属于一个调用方的响应无法通过缓存泄露给另一个。
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",
},
});
}
}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",
},
});
}
}Service binding 调用值得单独说明,因为你传入的 URL 含义可能与你想象的不同。
使用 fetch() 调用 service binding 时,URL 中的 hostname 是占位符。请求通过 binding 路由,而非 DNS——hostname 永远不会被解析。且由于 host 不是缓存键的一部分(如缓存属于 Worker,而非 domain所述),占位符对缓存也没有影响。只有路径(和查询字符串)与目标 entrypoint 和 ctx.props 一起构成缓存键:
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");
},
};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>;若希望不同调用方获得不同的缓存响应,请变化 ctx.props。若希望按请求区分,请变化 path 或 query string。变化 hostname 没有任何作用。
在发布时,两个信号可让你了解缓存行为:
-
Cf-Cache-Status响应头。 最常见的值包括HIT、MISS、EXPIRED、REVALIDATED、UPDATING、STALE和BYPASS。HIT表示 Cloudflare 返回缓存响应而未运行 Worker。MISS表示 Worker 运行且响应被存储。UPDATING表示缓存响应已过期,Worker 在后台运行以刷新它。BYPASS表示此请求的缓存被禁用。完整值集参见 Cloudflare 缓存响应。 -
Workers 可观测性仪表板中的缓存命中。 每次调用都会显示是否从缓存提供,因此你可以筛选并聚合 Worker 流量的缓存命中行为。
Cloudflare 目前不公开缓存键的构成本身。若两个你预期应共享缓存响应的请求没有共享,你需要根据缓存键包含哪些内容中列出的组件,推理键的哪一部分不同。常见缓存问题及诊断方法参见调试。
默认情况下,请求 URL 的路径和查询字符串构成缓存键的 URL 组件。当一个 entrypoint 通过 ctx.exports loopback 调用另一个缓存 entrypoint 时,调用 entrypoint 可通过在请求上设置 cf.cacheKey 覆盖该组件。
在下面的示例中,Backend entrypoint 是被缓存的那个。default entrypoint 通过 ctx.exports 将请求转发给它,并自行选择缓存键:
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 },
});
},
};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>;自定义缓存键替换缓存键中的路径和查询字符串。缓存键包含哪些内容中描述的其他一切仍然适用:
- 目标 entrypoint 与调用方的
ctx.props仍是键的一部分。自定义缓存键无法跨越 entrypoint 或ctx.props,因此上文描述的多租户隔离在调用方自行选择键时仍然成立。自定义键只能寻址被调用方自身缓存命名空间内的条目。 - URL 不同但
cf.cacheKey相同的两个请求解析到同一缓存条目。这是将多个 URL 合并到单一缓存响应的方式。 - URL 相同但
cf.cacheKey不同的两个请求解析到独立缓存条目。
将 cf.cacheKey 设为空字符串,或不设置,即可回退到默认的 URL 派生键。
在此模式中,default entrypoint 是应在每个请求上运行的 gateway,因此在其上禁用缓存,在 Backend 上保留(参见按 entrypoint 缓存):
{
"name": "my-worker",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-17",
"cache": { "enabled": true },
"exports": {
"default": { "type": "worker", "cache": { "enabled": false } },
"Backend": { "type": "worker", "cache": { "enabled": true } },
},
}name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-17"
[cache]
enabled = true
[exports.default]
type = "worker"
[exports.default.cache]
enabled = false
[exports.Backend]
type = "worker"
[exports.Backend.cache]
enabled = true- 忽略 URL 的部分内容。 剥离跟踪参数(
utm_source、gclid),或完全去掉 query string,使不改变响应的变体共享一个缓存条目。 - 基于 URL 以外的内容建键。 从 gateway Worker 信任的值构建键——例如规范化的资源标识符——使多个等效 URL 映射到一个条目。
- 自行划分缓存。 向键追加区分值(例如内容版本),为原本会冲突的请求强制产生独立条目。
对于按调用方隔离,继续使用 ctx.props,而非将调用方身份编码进缓存键——ctx.props 会自动成为键的一部分且无法绕过。
cf.cacheKey 仅在调用留在你的账户内时生效。当请求跨越账户边界时,Cloudflare 会丢弃 cf 对象——例如指向另一账户拥有的 Worker 的 service binding。此时自定义键会被忽略,缓存键回退到请求 URL,因此一个账户中的调用方永远无法影响(或探测)另一账户中 Worker 的缓存。
这也意味着 cf.cacheKey 对来自终端用户的请求无效。来自浏览器或 API 客户端的入站请求上的 cf 对象由 Cloudflare 填充,而非客户端,因此客户端无法设置自己的缓存键。