Your Worker can now have its own cache in front of it
Cloudflare launched Workers Cache, a tiered cache that sits in front of your Worker, enabled by a single line in wrangler.jsonc (`"cache": { "enabled": true }`). On a cache hit, the Worker does not run and CPU time is not billed; on a miss, the Worker runs and populates the cache. It supports stale-while-revalidate, Vary-based content negotiation, and multi-tenant isolation via ctx.props. Unlike zone-level caches, Workers Cache belongs to the Worker itself, working on custom domains, workers.dev, and Workers for Platforms. Most notably, the cache sits in front of every Worker entrypoint, including those invoked via service bindings and ctx.exports, enabling developers to compose an app as a chain of entrypoints with per-entrypoint caching control. Framework integrations (including Astro) are available.
2026-07-06
17 min read
Today we are launching Workers Cache: a tiered cache that sits in front of your Worker, configured by a single line of Wrangler config and the same Cache-Control headers you already know.
When Workers Cache is enabled, every cacheable request to your Worker hits Cloudflare's cache first. If there's a fresh cached response, Cloudflare returns it directly — your Worker doesn't run, and you don't pay CPU time for it. On a miss, your Worker runs, and if your response is cacheable, Cloudflare stores it for the next request. The next request from anywhere on Earth can be served straight from cache.
The whole thing is one config block:
今天我们正式推出 Workers Cache:一个位于 Worker 前方的分层缓存,只需一行 Wrangler 配置和你早已熟悉的 Cache-Control 头即可启用。
启用 Workers Cache 后,对 Worker 的每个可缓存请求都会首先到达 Cloudflare 的缓存。如果存在新鲜的缓存响应,Cloudflare 会直接返回——你的 Worker 不会执行,你也无需为此支付 CPU 时间。如果缓存未命中,你的 Worker 会执行一次,并且如果你的响应是可缓存的,Cloudflare 会将其存储起来以备后续请求。此后,来自全球任何地方的请求都可以直接从缓存中获取响应。
整个配置就是一个代码块:
{ "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2026-05-01", "cache": { "enabled": true } }
After that, you control caching the way HTTP has always wanted you to — by setting headers on your responses:
return new Response(body, { headers: { "Cache-Control": "public, max-age=300, stale-while-revalidate=3600", "Cache-Tag": "products,product:123", }, });
And when content changes, your Worker purges its own cache:
await ctx.cache.purge({ tags: ["product:123"] });
That's the whole API. There is no zone to configure, no rules engine to set up, no separate cache to provision, and no second product to log into. The Worker's code is the configuration surface, and the cache follows the Worker wherever it runs — on a custom domain, on workers.dev, behind a service binding, in a preview, in a Workers for Platforms tenant. One Worker, one cache, configured once.
{ "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2026-05-01", "cache": { "enabled": true } }
之后,你就可以像 HTTP 一直期望的那样控制缓存——在响应中设置请求头:
return new Response(body, { headers: { "Cache-Control": "public, max-age=300, stale-while-revalidate=3600", "Cache-Tag": "products,product:123", }, });
当内容变更时,你的 Worker 可以清理自己的缓存:
await ctx.cache.purge({ tags: ["product:123"] });
这就是整个 API。无需配置区域(zone),无需设置规则引擎(rules engine),无需额外调配缓存,也无需登录第二个产品。Worker 的代码就是配置面,缓存会跟随 Worker 在任何地方运行——无论是在自定义域名上、在 workers.dev 上、在服务绑定(service binding)背后、在预览环境中,还是在 Workers for Platforms 的租户中。一个 Worker,一个缓存,一次配置。
That's the surface area. There’s a lot underneath: tiered caching across our entire network, full support for stale-while-revalidate so stale responses never block a user, content negotiation via Vary, multi-tenant-safe cache keys via ctx.props, programmatic purges by tag or path prefix, and — the part we think is the biggest unlock — a cache that sits in front of every Worker entrypoint, not just the public one, with per-entrypoint control over which ones cache and which don't. That last piece means you can compose caching directly into the structure of your app: a chain of entrypoints with cache stages slotted in wherever you want them, configured by the code on either side. We'll walk through all of it below.
Workers Cache is available today to every Worker on any plan, enabled in Wrangler.
When we introduced Workers in 2017, the pitch was that you could run code on Cloudflare's network to transform requests on their way to your origin. The Worker sat in front of the cache and the origin.
But the world changed. Workers stopped being a thing you bolted onto an origin and started being the origin. Frameworks like Astro, TanStack Start, Next.js, Remix, and SvelteKit all ship a Cloudflare adapter that builds your app as a Worker. There's no origin behind them. The Worker is the server.
When the Worker is the origin, the original architecture has nothing to cache. Every request runs your code, even when the response would be byte-for-byte identical to the one you returned a second ago. The Workers runtime is fast enough that this works — it routinely handles tens of millions of requests per second without breaking a sweat — but "fast enough to render every request" still costs you latency on every page load and CPU time on every invocation. And on a server-rendered app, every page load is, by definition, a render.
Workers Cache flips the architecture. Cloudflare's cache now sits in front of the Worker.
以上就是表层功能。其底层机制非常丰富:跨整个网络的分层缓存、对 stale-while-revalidate 的全面支持以确保过期响应不会阻塞用户、通过 Vary 进行内容协商、通过 ctx.props 实现多租户安全的缓存键、按标签或路径前缀进行程序化清理,以及我们认为最大的突破点——一个位于每个 Worker 入口点(不仅是公开入口点)前方的缓存,并且可以按入口点控制哪些进行缓存、哪些不进行缓存。最后这一点意味着你可以将缓存直接编排到应用的结构中:一系列入口点,你可以将缓存层插入到任何你想要的位置,由其两侧的代码进行配置。下面我们将逐一详细介绍。
Workers Cache 今日起对任何套餐上的所有 Worker 开放,通过 Wrangler 即可启用。
当我们在 2017 年推出 Workers 时,其定位是你可以在 Cloudflare 网络上运行代码,在请求到达你的源站(origin)之前对其进行转换。Worker 位于缓存和源站之前。
但世界已经改变了。Worker 不再是你附加在源站上的东西,它自己开始成为源站。像 Astro、TanStack Start、Next.js、Remix 和 SvelteKit 这样的框架都提供了 Cloudflare 适配器,可以将你的应用构建成一个 Worker。它们背后没有源站,Worker 本身就是服务器。
当 Worker 成为源站时,原有的架构就没有什么可缓存的了。每个请求都会运行你的代码——即使返回的响应与一秒之前返回的响应字节完全相同。Workers 运行时足够快,这确实可行——它通常每秒处理数千万次请求而毫不费力——但
This is what was missing for server-side rendering on Workers. You used to have to choose between two unsatisfying options:
Prerender everything at build time ("static site generation"). Fast page loads, but every change requires a full rebuild and redeploy. For a docs site with a few thousand pages, that's 5–10 minutes. For a large e-commerce site, it's worse — and the build runs every single time you touch anything.
Render every page on every request. Up-to-date content, but every page load pays the rendering cost and every visitor pays the latency.
Workers Cache gives you a third option: server-render on demand, cache the rendered response, refresh it on a time-to-live (TTL) you choose. The first request to a new page still renders. Every subsequent request, until the cache expires, is served as if the page were static. When the cache expires, the next request triggers a re-render — and with stale-while-revalidate, even that one doesn't wait.
You get the speed of a static site without the build time, and the freshness of server rendering without the cost. No framework-specific machinery like Incremental Static Regeneration. Just HTTP caching, working the way it was designed to work, in front of code that was designed to be the origin.
这正是 Workers 上进行服务端渲染所缺失的功能。过去你只能在两个令人不满意的选项中选择:
在构建时预渲染所有内容(
The stale-while-revalidate directive tells Cloudflare that when a cached response expires, it's allowed to serve the stale copy immediately while it refreshes the response in the background. Cloudflare shipped full support for stale-while-revalidate earlier this year, and it's the directive that turns "we cache your Worker" into "your Worker's site feels static."
Without it, the first request after a cache entry expires has to wait for the Worker to render the page from scratch. The user sees that latency. With it, the first request after expiration gets the stale page immediately (with a Cf-Cache-Status: UPDATING header), and the Worker runs in the background to refill the cache. Every user, including the one who triggered the refresh, gets a cache-speed response.
In practice, this looks like:
export default { async fetch(request) { const html = await renderPage(request); return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8", // Treat as fresh for 5 minutes; serve stale for up to an hour // while a background refresh runs. "Cache-Control": "public, max-age=300, stale-while-revalidate=3600", }, }); }, };
The mental model that makes this click:
Fresh window (max-age): Cloudflare serves the cached response. Your Worker doesn't run.
Stale window (stale-while-revalidate): Cloudflare serves the cached response. Your Worker runs in the background to refresh it. No user waits.
Outside both windows: Cloudflare runs your Worker to generate a fresh response, and the user waits for that one render.
You pick the windows.
stale-while-revalidate 指令告诉 Cloudflare,当缓存的响应过期后,允许立即提供过期的副本,同时在后台刷新响应。Cloudflare 在今年早些时候提供了对 stale-while-revalidate 的全面支持。正是这个指令,将“我们缓存你的 Worker”变成了“你的 Worker 站点感觉像静态站点”。
如果没有它,缓存条目过期后的第一个请求必须等待 Worker 从头开始渲染页面,用户会看到这个延迟。有了它,过期后的第一个请求会立即获得过期页面(带有 Cf-Cache-Status: UPDATING 请求头),同时 Worker 在后台运行以重新填充缓存。每个用户,包括触发刷新的那个,都会获得缓存级别的快速响应。
在实践中,它看起来像这样:
export default { async fetch(request) { const html = await renderPage(request); return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8", // 将内容视为新鲜 5 分钟;在后台刷新期间最多提供过期版本 1 小时 "Cache-Control": "public, max-age=300, stale-while-revalidate=3600", }, }); }, };
理解它的心智模型如下:
新鲜窗口(max-age):Cloudflare 提供缓存的响应。你的 Worker 不运行。
过期窗口(stale-while-revalidate):Cloudflare 提供缓存的响应。你的 Worker 在后台运行以刷新它。没有用户需要等待。
超出两个窗口:Cloudflare 运行你的 Worker 生成一个新响应,用户需要等待这一次渲染。
你可以自行选择窗口大小。
Real apps rarely return the same bytes to every client. The same product page might be HTML for a browser and JSON for an API client. The same image might be WebP for clients that support it and JPEG for the ones that don't. The same homepage might come back in English, French, or Japanese depending on the user.
Doing this without a cache is easy — your Worker just reads the request header and returns the right thing. Doing it with a cache is where it usually gets ugly. Most caches give you two bad options: cache nothing on URLs that have multiple representations, or cache one representation and serve it to everyone.
Workers Cache supports the standard HTTP Vary header, which is the right way to solve this. When your Worker returns a response with Vary: Accept-Encoding (or Accept, or Accept-Language, or any other request header), Cloudflare stores a separate cached variant per distinct combination of those headers — and only returns a variant whose stored values match the incoming request.
export default { async fetch(request) { const accept = request.headers.get("Accept") ?? ""; const wantsWebp = accept.includes("image/webp");
const body = wantsWebp ? await fetchWebpImage() : await fetchJpegImage();
return new Response(body, {
headers: {
"Content-Type": wantsWebp ? "image/webp" : "image/jpeg",
"Cache-Control": "public, max-age=3600",
// Cache a separate variant per distinct Accept header value.
Vary: "Accept",
},
});
}, };
One URL, two cached variants. A browser that sends Accept: image/webp,/ gets the WebP. A browser that sends Accept: image/jpeg gets the JPEG. Both come from cache. Your Worker writes both variants on the first request to each, and then runs zero times for either after that.
This is the well-trodden HTTP standard for content negotiation, and Workers Cache implements it the way RFC 9110 and RFC 9111 describe. There's no allowlist of what headers you can Vary on. You list whatever you need, and Cloudflare keys variants on the verbatim values.
实际的应用很少会给每个客户端返回完全相同的字节。同一个产品页面,可能对浏览器返回 HTML,对 API 客户端返回 JSON。同一张图片,可能对支持 WebP 的客户端返回 WebP 格式,对不支持则返回 JPEG。同一个首页,可能根据用户返回英文、法文或日文。
在没有缓存的情况下做这件事很容易——你的 Worker 只需读取请求头并返回正确的内容。但在有缓存的情况下做这件事通常会很棘手。大多数缓存给你两个糟糕的选择:对具有多种表示的 URL 什么都不缓存,或者缓存一种表示然后把它返回给所有人。
Workers Cache 支持标准的 HTTP Vary 请求头,这是解决该问题的正确方式。当你的 Worker 返回一个带有 Vary: Accept-Encoding(或 Accept、Accept-Language,或任何其他请求头)的响应时,Cloudflare 会针对这些请求头的每种不同组合存储一个单独的缓存变体——并且仅在其存储的值与传入请求匹配时才返回该变体。
export default { async fetch(request) { const accept = request.headers.get("Accept") ?? ""; const wantsWebp = accept.includes("image/webp");
const body = wantsWebp ? await fetchWebpImage() : await fetchJpegImage();
return new Response(body, {
headers: {
"Content-Type": wantsWebp ? "image/webp" : "image/jpeg",
"Cache-Control": "public, max-age=3600",
// 针对每个不同的 Accept 请求头值缓存一个单独的变体
Vary: "Accept",
},
});
}, };
一个 URL,两个缓存的变体。发送 Accept: image/webp,/ 的浏览器获得 WebP。发送 Accept: image/jpeg 的浏览器获得 JPEG。两者都来自缓存。你的 Worker 仅在对每个变体的第一次请求时写入,之后对任意一个都不再运行。
这是 HTTP 内容协商久经考验的标准,Workers Cache 按照 RFC 9110 和 RFC 9111 描述的方式实现了它。没有任何关于你可以对哪些请求头进行 Vary 的允许列表。你列出任何你需要的请求头,Cloudflare 就会根据其确切值来键控变体。
Before we get to what becomes possible with all this, there's a conceptual shift worth naming.
Cloudflare has had a cache forever. It's configured at the zone level: Cache Rules, Page Rules, the cached-file-extensions list, Cache Reserve, Tiered Cache topology, custom cache keys. All of it is set per zone, and historically a Worker had to either fit into that zone's configuration or work around it.
Workers Cache is different. It's your Worker's cache — it belongs to the Worker, not to a zone. This has a bunch of consequences that turn out to matter:
There is no zone configuration to manage. Cache Rules, cache level settings, the file-extensions list, Page Rules — none of them apply to Workers Cache. The Worker's Cache-Control headers are the configuration.
The cache follows the Worker, not the hostname. A Worker that's bound to api.example.com, api.example.net, and invoked over a service binding shares one cache across all three. A request to /users/42 hits the same cached entry regardless of which way in it came.
The cache works on workers.dev. It works in preview URLs (each preview gets its own cache, so testing a change doesn't poison production). It works in Workers for Platforms (each user Worker has its own cache, isolated from the dispatcher and from other tenants). All of these used to be second-class citizens for caching. They aren't anymore.
Purges are scoped to the Worker’s entrypoint. When you call ctx.cache.purge({ purgeEverything: true }), you're only purging your Worker entrypoint's cache. No risk of nuking your zone's other content. No risk of one Worker's deploy invalidating another's data.
What you configure about caching, you configure in code: which paths get longer TTLs (branch on the path and set a different max-age), which requests bypass the cache (return Cache-Control: private), how the cache key is shaped (control what gets into ctx.props, normalize the URL in a gateway Worker before dispatching). The Worker you already wrote is the configuration surface.
在探讨所有这一切带来的可能性之前,有必要指出一个概念上的转变。
Cloudflare 一直以来都有缓存功能。它是在区域(zone)级别配置的:缓存规则(Cache Rules)、页面规则(Page Rules)、可缓存的文件扩展名列表、缓存预留(Cache Reserve)、分层缓存拓扑、自定义缓存键。所有这些都是在每个区域上设置的,历史上 Worker 要么必须适应那个区域的配置,要么绕开它。
Workers Cache 则不同。它是属于你的 Worker 的缓存——它归属于 Worker,而非区域。这带来了一系列重要的后果:
无需管理区域配置。缓存规则、缓存级别设置、文件扩展名列表、页面规则——这些都不适用于 Workers Cache。Worker 的 Cache-Control 请求头就是配置本身。
缓存跟随 Worker,而非主机名。一个绑定到 api.example.com 和 api.example.net 并通过服务绑定调用的 Worker,在这三种方式之间共享同一个缓存。对 /users/42 的请求,无论从哪个入口进入,都会命中同一个缓存条目。
缓存在 workers.dev 上可用。它在预览 URL 中也可用(每个预览都有自己的缓存,因此测试更改不会污染生产环境)。它在 Workers for Platforms 中也可用(每个用户 Worker 都有自己的缓存,与调度程序和其他租户隔离)。所有这些在过去对于缓存都是二等公民,现在不再是了。
清理操作限定在 Worker 的入口点范围内。当你调用 ctx.cache.purge({ purgeEverything: true }) 时,你只清理的是你的 Worker 入口点的缓存。没有破坏区域中其他内容的风险。没有因为一个 Worker 的部署而导致另一个 Worker 的数据失效的风险。
你关于缓存的所有配置都在代码中完成:哪些路径获得更长的 TTL(在路径上分支并设置不同的 max-age),哪些请求绕过缓存(返回 Cache-Control: private),如何塑造缓存键(控制哪些内容进入 ctx.props,在网关 Worker 中规范化 URL 后再分发)。你已编写好的 Worker 就是配置面。
Workers Cache is regionally tiered by default. There are two layers:
A lower tier in the Cloudflare data center closest to the user. Every data center that receives traffic for your Worker has its own lower-tier cache.
An upper tier that aggregates fills across the whole network. There are fewer of these, and every lower tier consults the upper tier on a miss.
A request hits the lower tier first. On a hit, the response is served and that's the end of it. On a miss, the lower tier asks the upper tier. On a hit there, the response is returned and also stored in the lower tier on the way back. Only if both tiers miss does your Worker actually run — and the response from that run gets stored in both tiers.
The reason this matters is that the first request anywhere in the world populates the upper tier. Every subsequent request, from any data center, can be served from the upper tier without your Worker running — even if the lower tier at that data center has never seen the request before. Cache hit ratios are dramatically higher than they would be with a single flat cache layer, which is exactly what you want when your Worker is the origin.
This is the same topology that powers Tiered Cache for zones today, except you don't configure it. There is no dialog for "turn on tiered cache for my Worker." Every Worker that has caching enabled gets tiering for free.
Workers Cache 默认启用区域分层缓存。它包含两层:
较低层位于离用户最近的 Cloudflare 数据中心。每个接收你 Worker 流量的数据中心都有自己的较低层缓存。
较高层在整个网络中聚合填充。较高层的数量较少,每个较低层在未命中时会咨询较高层。
请求首先到达较低层。如果命中,则直接返回响应,流程结束。如果未命中,较低层会向较高层查询。如果较高层命中,则返回响应,并在返回途中存储到较低层。仅当两层都未命中时,你的 Worker 才会实际运行——并且那次运行的响应会被存储到两层中。
这之所以重要,是因为世界各地的第一次请求会填充较高层。之后来自任何数据中心的后续请求都可以从较高层获取响应,而无需运行你的 Worker——即使那个数据中心的较低层之前从未见过该请求。缓存命中率会远高于单一的扁平缓存层,这正是当你的 Worker 作为源站时所期望的。
这与当前为区域提供动力的分层缓存(Tiered Cache)拓扑相同,只是你无需配置它。没有“为我的 Worker 开启分层缓存”这样的对话框。启用了缓存功能的每个 Worker 都可以免费获得分层缓存。
There's a recurring tension in web performance that nobody has fully resolved: you want your code to run close to the user (because the round-trip between user and server is on the critical path), and you want your code to run close to the data (because every database query is also a round-trip). Pick one, and the other gets slow.
We've spent years chasing both. Our network puts us within ~50ms of about 95% of the world's Internet users. Smart Placement and Placement Hints let you keep your code close to your data without ever having to think about cloud regions. But until now, the two pieces didn't fully compose. You could do "near the user" or "near the data," and if you wanted both halves of your app to be in the right place at the same time, you had to be a Cloudflare expert. We knew we could do better.
Workers Cache is the piece that closes the gap. Because the cache belongs to the Worker (not the zone), and because service bindings and ctx.exports calls between Workers go through the cache, you can build an app as a chain of Workers — each one running where it should run — with the cache as the seam between them.
The architecture looks like this:
Worker A runs near the user. It handles the cheap, latency-sensitive parts of every request: authentication, rate limiting, routing, header normalization, rendering the outer "shell" of an HTML page that doesn't depend on data.
Worker B runs near the data, courtesy of Smart Placement or an explicit Placement Hint. It does the heavy work: server-rendering pages that fetch data, reading product catalogs, generating search results, aggregating APIs, expensive transforms.
Workers Cache sits in front of Worker B. When Worker A calls Worker B over a service binding, Cloudflare checks Worker B's cache first. On a hit, Worker A receives the response and Worker B doesn't run at all — no data-center hop, no database query, no rendering work.
The cache hit path becomes: user → Worker A near the user → cache hit for Worker B → response. The data hop is paid only on a miss. Your hot pages run at the speed of code-in-front-of-the-user, and your cold pages still benefit from running near the data when they do execute.
You don't have to architect anything special to get this. Write your app as two Workers, point one at the other with a service binding, turn caching on in Worker B’s wrangler.jsonc file, and you're done.
Web 性能中存在一个反复出现的、尚未被完全解决的矛盾:你希望代码靠近用户运行(因为用户与服务器之间的往返延迟在关键路径上),同时你又希望代码靠近数据运行(因为每次数据库查询也是一次往返)。两者只能选其一,另一个就会变慢。
多年来我们一直在追求两者兼顾。我们的网络使我们可以与世界范围内约 95% 的互联网用户在 ~50ms 以内。智能放置(Smart Placement)和放置提示(Placement Hints)让你无需考虑云区域就能让代码靠近数据。但直到现在,这两部分还无法完全组合。你可以做到“靠近用户”或“靠近数据”,但如果你希望应用的两部分同时处于正确的位置,你必须是一名 Cloudflare 专家。我们知道我们可以做得更好。
Workers Cache 是弥合这一差距的关键。由于缓存归属于 Worker(而非区域),并且由于 Worker 之间的服务绑定和 ctx.exports 调用会经过缓存,你可以将应用构建成一个 Worker 链——每个 Worker 运行在其应该在的位置——而缓存就成为它们之间的接口。
该架构如下所示:
Worker A 靠近用户运行。它处理每个请求中成本较低、对延迟敏感的部分:身份验证、速率限制、路由、请求头规范化、渲染不依赖数据的 HTML 页面外层“壳”。
Worker B 靠近数据运行,借助智能放置或显式的放置提示。它处理繁重的工作:服务端渲染需要获取数据的页面、读取产品目录、生成搜索结果、聚合 API、昂贵的转换。
Workers Cache 位于 Worker B 的前方。当 Worker A 通过服务绑定调用 Worker B 时,Cloudflare 首先检查 Worker B 的缓存。如果命中,Worker A 获得响应,Worker B 完全不运行——没有数据中心跳转,没有数据库查询,没有渲染工作。
命中的路径变为:用户 → 靠近用户的 Worker A → Worker B 缓存命中 → 响应。仅在未命中时才会付出数据中心的跳转成本。你的热门页面以代码靠近用户的速度运行,而你的冷门页面在执行时仍能从靠近数据运行中受益。
你无需进行任何特殊的架构设计来实现这一点。将你的应用写成两个 Worker,通过服务绑定将一个指向另一个,在 Worker B 的 wrangler.jsonc 文件中启用缓存,就完成了。
If you're caching a Worker that returns user-specific data — say, an API that serves different content per logged-in user — you need a way to make sure one user can never see another user's cached response. The standard solution is "don't cache authenticated requests," and Cloudflare's automatic bypass for Authorization headers does exactly that. But "don't cache anything" gives up the entire performance win.
Workers Cache solves this by making the caller's ctx.props part of the cache key. When one Worker calls another over a service binding and passes ctx.props with a user ID, tenant ID, or any other identifier, callers with different props get separate cache entries. One user's response can never leak into another user's cache.
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 part of the cache key. User A and User B // requesting the same URL get separate cached entries. const { userId } = this.ctx.props; const data = await loadUserData(userId);
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=300",
},
});
} }
The typical pattern is to authenticate the request in a gateway Worker, strip the Authorization header, set the authenticated user's ID into ctx.props, and then call the cached backend Worker. The gateway runs on every request (it has to, to authenticate), but the expensive backend only runs when there's no cache entry for that user yet. Auth'd APIs go from "uncacheable" to "cached per user with full safety," and the cache key does the isolation for you. The docs walk through this in detail in Multi-tenant safety with ctx.props and the example in Per-user authenticated responses.
Other CDNs make you choose between correctness and hit ratio: key the cache by each user’s token, or send every request back to origin for authorization. Workers Cache lets you share cached API responses at the edge while preserving per-request authorization boundaries. We don’t know of another CDN that offers this as a built-in model for authenticated, multi-tenant APIs. We’re pretty proud of it.
如果你正在缓存一个返回用户特定数据的 Worker——比如说,一个为每个登录用户提供不同内容的 API——你需要一种方法来确保一个用户永远不会看到另一个用户的缓存响应。标准的解决方案是“不缓存经过身份验证的请求”,Cloudflare 对 Authorization 请求头的自动绕过正是这样做的。但“不缓存任何东西”会让你放弃全部的性能优势。
Workers Cache 通过将调用者的 ctx.props 纳入缓存键来解决这个问题。当一个 Worker 通过服务绑定调用另一个 Worker,并传递带有用户 ID、租户 ID 或其他标识符的 ctx.props 时,具有不同 props 的调用者会获得不同的缓存条目。一个用户的响应永远不会泄漏到另一个用户的缓存中。
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 是缓存键的一部分。用户 A 和用户 B // 请求同一个 URL 会获得不同的缓存条目。 const { userId } = this.ctx.props; const data = await loadUserData(userId);
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=300",
},
});
} }
典型的模式是在一个网关 Worker 中验证请求,剥离 Authorization 请求头,将已验证的用户 ID 设置到 ctx.props 中,然后调用缓存的后端 Worker。网关在每个请求上都运行(它必须这样做来进行身份验证),但昂贵的后端仅在尚未为该用户建立缓存条目时才运行。经过身份验证的 API 从“不可缓存”转变为“按用户安全缓存”,缓存键为你完成了隔离。文档在“使用 ctx.props 实现多租户安全”和“按用户进行身份验证的响应”示例中详细介绍了这一点。
其他 CDN 让你在正确性和命中率之间做出选择:要么按每个用户的令牌键控缓存,要么将每个请求都发回源站进行授权。Workers Cache 让你在边缘共享缓存的 API 响应,同时保留每个请求的授权边界。我们不知道还有其他 CDN 能够为经过身份验证的多租户 API 将这种模式作为内置功能提供。我们对此感到非常自豪。
Here is the part of Workers Cache that we think is the biggest unlock, and it's the part that's hardest to see if you're thinking about it as "a CDN cache that happens to work in front of Workers."
Workers Cache sits in front of every Worker entrypoint — the default export, every named WorkerEntrypoint, and every call between entrypoints in the same Worker via ctx.exports. That last clause is the one that changes what you can build.
When one entrypoint calls another via ctx.exports, the cache evaluates that call the same way it would evaluate a request from a browser. A hit returns the cached response and the callee never runs. A miss runs the callee and stores its response under its own cache key — keyed by the callee's entrypoint, path, query string, and ctx.props. The caller still runs on every request, but anything it hands off to the callee is memoized independently.
You decide, per entrypoint, which ones cache. In your Wrangler config, the exports map lets you turn caching on or off for each entrypoint by name ("default" is the default export). Opt an entrypoint in to cache the responses it produces; opt one out to keep it running on every request. A gateway or router entrypoint — anything that authenticates, normalizes, or dispatches — should be opted out, so it always runs, and its own output is never served from cache.
That gives you a primitive you can compose. You can author a Worker as a chain of small entrypoints — auth, normalization, routing, the expensive read, the data layer — and let Workers Cache slot in wherever you want it. Each cached entrypoint is a unit of memoization with its own key, its own TTL, and its own tag namespace for purging. Anything you would want to configure about caching — when it runs, what it keys on, when it invalidates — is expressed as ordinary Worker code: which entrypoint you call, what request you forward, what ctx.props you pass, what Cache-Control you set.
这是 Workers Cache 中我们认为最大的突破点,也是如果你只把它看作“恰好在 Worker 前工作的一个 CDN 缓存”时最难看到的部分。
Workers Cache 位于每个 Worker 入口点的前方——包括默认导出、每个命名的 WorkerEntrypoint,以及同一 Worker 内通过 ctx.exports 进行的入口点之间的每次调用。最后这一条款改变了你能构建的东西。
当一个入口点通过 ctx.exports 调用另一个入口点时,缓存会像评估来自浏览器的请求一样评估该调用。命中则返回缓存的响应,被调用者完全不运行。未命中则运行被调用者,并将其响应存储在其自己的缓存键下——该缓存键由被调用者的入口点、路径、查询字符串和 ctx.props 确定。调用者在每个请求上仍然运行,但它交给被调用者的任何内容都是独立记忆化的。
你可以按入口点决定哪些需要缓存。在你的 Wrangler 配置中,exports 映射允许你按名称为每个入口点打开或关闭缓存(“default”代表默认导出)。将一个入口点加入缓存以缓存它产生的响应;排除一个入口点以使其在每个请求上都运行。网关或路由器入口点——任何进行身份验证、规范化或分发的入口点——应该被排除,以便它始终运行,并且它自己的输出永远不会从缓存中提供。
这为你提供了一个可以组合的原语。你可以将 Worker 编写为一系列小入口点的链——身份验证、规范化、路由、昂贵的数据读取、数据层——并让 Workers Cache 插入到你想要的任何位置。每个缓存的入口点都是一个记忆化单元,拥有自己的键、自己的 TTL 和用于清理的自己的标签命名空间。你想要配置的关于缓存的任何内容——何时运行、键控什么、何时失效——都以普通的 Worker 代码来表达:你调用哪个入口点、转发哪个请求、传递什么 ctx.props、设置什么 Cache-Control。
To make this concrete, here's a single Worker that does three things you couldn't easily do together on any other platform: it authenticates every request, caches the expensive backend behind a multi-tenant-safe cache key, and invalidates that cache when data changes.
Caching is configured per entrypoint. The gateway must run on every request — both to authenticate and because a cached gateway response would skip that auth check — so we disable caching on the default entrypoint and enable it only on the inner one:
{ "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2026-05-01", "cache": { "enabled": true }, "exports": { // The gateway runs on every request — don't cache it. "default": { "type": "worker", "cache": { "enabled": false } }, // Cache the expensive inner entrypoint. "CachedBackend": { "type": "worker", "cache": { "enabled": true } } } }
import { WorkerEntrypoint } from "cloudflare:workers";
interface Env { API_TOKEN: string; } interface Props { userId: string; }
// Inner entrypoint: the expensive work. Workers Cache sits in front // of this — on a hit, this code never runs. export class CachedBackend extends WorkerEntrypoint<Env, Props> { async fetch(request: Request): Promise<Response> { // ctx.props.userId is part of the cache key, so this is cached // separately for every user. const { userId } = this.ctx.props; const data = await loadExpensiveData(userId);
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
"Cache-Tag": `user:${userId}`,
},
});
}
// Invalidate a user's cached response. purge() is scoped to the
// entrypoint that calls it, so it must run inside CachedBackend —
// the entrypoint that owns the cached response.
async invalidate(userId: string): Promise<void> {
await this.ctx.cache.purge({ tags: [user:${userId}] });
}
}
// Outer entrypoint: runs on every request to authenticate and route. // Caching is disabled for it in Wrangler config (above), so it always // runs and the auth check is never skipped by a cache hit. export default { async fetch(request, env, ctx): Promise<Response> { const userId = await authenticate(request, env); if (!userId) return new Response("Unauthorized", { status: 401 });
// Invalidate this user's cache on writes, from the entrypoint that
// owns it.
if (request.method === "POST") {
await handleWrite(request, userId);
await ctx.exports.CachedBackend.invalidate(userId);
return new Response("OK");
}
// For reads: strip Authorization (otherwise Cloudflare's automatic
// bypass fires and nothing caches), then dispatch to the cached
// backend with the authenticated user's identity in ctx.props.
const forwarded = new Request(request);
forwarded.headers.delete("Authorization");
return ctx.exports.CachedBackend.fetch(forwarded, {
props: { userId },
});
}, } satisfies ExportedHandler<Env>;
The whole thing is one Worker. One source file. One deploy. But there are two execution stages — caching is turned off for the gateway and on for the backend in one small exports block — and a cache sits between them, keyed per user, invalidated by the write path, and serving stale during background refreshes.
为了具体说明,这是一个单一的 Worker,它做了三件你在其他任何平台上都难以同时做到的事情:它验证每个请求,在多租户安全的缓存键后面缓存昂贵的后端,并在数据更改时使该缓存失效。
缓存是按入口点配置的。网关必须在每个请求上运行——既是为了进行身份验证,也是因为如果缓存了网关响应,就会跳过该身份验证检查——因此我们在默认入口点上禁用缓存,并仅在内层入口点上启用:
{ "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2026-05-01", "cache": { "enabled": true }, "exports": { // 网关在每个请求上运行——不要缓存它。 "default": { "type": "worker", "cache": { "enabled": false } }, // 缓存昂贵的内层入口点。 "CachedBackend": { "type": "worker", "cache": { "enabled": true } } } }
import { WorkerEntrypoint } from "cloudflare:workers";
interface Env { API_TOKEN: string; } interface Props { userId: string; }
// 内层入口点:执行昂贵的工作。Workers Cache 位于其前方—— // 命中时,此代码永远不会运行。 export class CachedBackend extends WorkerEntrypoint<Env, Props> { async fetch(request: Request): Promise<Response> { // ctx.props.userId 是缓存键的一部分,因此会为每个用户 // 分别缓存。 const { userId } = this.ctx.props; const data = await loadExpensiveData(userId);
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
"Cache-Tag": `user:${userId}`,
},
});
}
// 使某个用户的缓存响应失效。purge() 限定在调用它的入口点范围内,
// 因此它必须在 CachedBackend 内部运行——即拥有该缓存响应的入口点。
async invalidate(userId: string): Promise<void> {
await this.ctx.cache.purge({ tags: [user:${userId}] });
}
}
// 外层入口点:在每个请求上运行以进行身份验证和路由。 // 在 Wrangler 配置(如上)中为其禁用了缓存,因此它始终运行, // 并且身份验证检查永远不会被缓存命中跳过。 export default { async fetch(request, env, ctx): Promise<Response> { const userId = await authenticate(request, env); if (!userId) return new Response("Unauthorized", { status: 401 });
// 在写入时,从拥有该缓存的入口点使此用户的缓存失效。
if (request.method === "POST") {
await handleWrite(request, userId);
await ctx.exports.CachedBackend.invalidate(userId);
return new Response("OK");
}
// 对于读取:剥离 Authorization(否则 Cloudflare 的自动
// 绕过会触发,导致任何内容都不缓存),然后将已验证用户的
// 身份通过 ctx.props 分派到缓存的后端。
const forwarded = new Request(request);
forwarded.headers.delete("Authorization");
return ctx.exports.CachedBackend.fetch(forwarded, {
props: { userId },
});
}, } satisfies ExportedHandler<Env>;
整个东西就是一个 Worker。一个源文件。一次部署。但存在两个执行阶段——在小小的 exports 块中,网关禁用了缓存,后端启用了缓存——一个缓存位于它们之间,按用户键控,通过写入路径失效,并在后台刷新期间提供过期内容。
The patterns this composes into are open-ended. The same shape works for:
Caching a Durable Object. Wrap the Durable Object behind an entrypoint, set Cache-Control on the response, and reads stop touching the Durable Object on a hit. Writes go to the DO directly and purge the cache by tag. The DO stays unaware that caching is happening.
Normalizing Accept-Encoding before Vary. The outer entrypoint restores the original encoding from request.cf.clientAcceptEncoding (Cloudflare's front line normalizes it for cache efficiency) and forwards to a cached entrypoint that varies on the real value. Hit ratios stay high; clients get the right encoding.
Stripping tracking parameters before caching. The outer entrypoint canonicalizes the URL — or sets a custom cache key with cf.cacheKey on the ctx.exports call — so the cached inner entrypoint sees only the canonical form, and ?utm_source=anything collapses to a single cache entry.
Stack them. A single Worker can have an outer entrypoint that authenticates and routes, a normalization entrypoint that strips tracking parameters and restores encoding headers, a cached entrypoint that fronts a Durable Object, and a separate cached entrypoint for an unauthenticated public API — each connected by a cache stage you didn't configure, just decided where to put. The Examples page in the docs walks through several of these end-to-end.
We don't know of another platform where you can do this. CDN caches sit in front of an origin. Function platforms run functions. We don't know of another platform that gives you a cache that sits inside a single deployable unit, between the parts of your application, with each cache stage configured by the code on either side of it. That's what Workers Cache is. And because it composes with everything else the platform already gives you — Smart Placement, Durable Objects, service bindings, ctx.props, ctx.exports — the patterns you can build are open-ended. We've barely scratched the surface in this post.
由此组合出的模式是开放式的。相同的结构同样适用于:
缓存 Durable Object。将 Durable Object 包装在一个入口点后面,在响应上设置 Cache-Control,那么读取操作在命中时就不会触及 Durable Object。写入操作直接发送给 DO 并按标签清理缓存。DO 完全不知道缓存的存在。
在 Vary 之前规范化 Accept-Encoding。外层入口点从 request.cf.clientAcceptEncoding(Cloudflare 的入口为了缓存效率已经将其规范化)恢复原始的编码,并将其转发到一个基于真实值进行 Vary 的缓存入口点。命中率保持很高;客户端获得正确的编码。
在缓存之前去除跟踪参数。外层入口点规范化 URL——或者在 ctx.exports 调用上使用 cf.cacheKey 设置自定义缓存键——这样内层缓存入口点只看到规范形式,?utm_source=anything 都归并到单个缓存条目。
将它们堆叠起来。一个单一的 Worker 可以拥有一个进行身份验证和路由的外层入口点、一个去除跟踪参数并恢复编码请求头的规范化入口点、一个位于 Durable Object 前的缓存入口点,以及一个用于未经身份验证的公共 API 的独立缓存入口点——每个都由一个你无需配置、只需决定放在何处的缓存阶段连接。文档中的“示例”页面端到端地介绍了其中的几个。
我们不知道还有其他平台可以做到这一点。CDN 缓存位于源站之前。函数平台运行函数。我们不知道还有其他平台能够为你提供这样一个位于单个可部署单元内部、位于你应用程序各部分之间的缓存,并且每个缓存阶段由其两侧的代码配置。这就是 Workers Cache。而且因为它可以与平台已经提供给你的所有其他功能——智能放置、Durable Objects、服务绑定、ctx.props、ctx.exports——组合,你可以构建的模式是开放式的。我们在这篇文章中只是浅尝辄止。
If you're building with Astro, the Cloudflare adapter wires up Workers Cache for you. Just add the cacheCloudflare provider to your configuration:
// astro.config.mjs import { defineConfig } from "astro/config"; import cloudflare from "@astrojs/cloudflare"; import { cacheCloudflare } from "@astrojs/cloudflare/cache";
export default defineConfig({ adapter: cloudflare(), output: "server", experimental: { cache: { provider: cacheCloudflare() }, routeRules: { "/products/": { maxAge: 300, swr: 3600, tags: ["products"] }, "/blog/": { maxAge: 60, swr: 86400, tags: ["blog"] }, }, }, });
The adapter enables the cache, sets the right headers on the responses Astro generates, attaches Cache-Tag values for invalidation, and gives you a cache.invalidate() helper for purging tags when content changes. Astro pages that opt into server rendering automatically get the "render once, cache, refresh in the background" flow described above — no per-route configuration required, no framework-specific runtime layer to learn.
We're working with the maintainers of other frameworks to ship the same integration. If you build a framework adapter for Cloudflare, the Workers Cache APIs are exactly what you'd want them to be — header-driven configuration, programmatic purges, no platform-specific concepts to model.
如果你使用 Astro 构建,Cloudflare 适配器会自动为你挂接 Workers Cache。只需将 cacheCloudflare 提供程序添加到你的配置中即可:
// astro.config.mjs import { defineConfig } from "astro/config"; import cloudflare from "@astrojs/cloudflare"; import { cacheCloudflare } from "@astrojs/cloudflare/cache";
export default defineConfig({ adapter: cloudflare(), output: "server", experimental: { cache: { provider: cacheCloudflare() }, routeRules: { "/products/": { maxAge: 300, swr: 3600, tags: ["products"] }, "/blog/": { maxAge: 60, swr: 86400, tags: ["blog"] }, }, }, });
该适配器启用缓存、在 Astro 生成的响应上设置正确的请求头、附加 Cache-Tag 值用于失效,并为你提供一个 cache.invalidate() 辅助函数,用于在内容更改时清理标签。选择服务端渲染的 Astro 页面会自动获得前面描述的“渲染一次、缓存、后台刷新”流程——无需逐路由配置,无需学习框架特定的运行时层。
我们正在与其他框架的维护者合作,以提供相同的集成。如果你为 Cloudflare 构建框架适配器,Workers Cache API 正是你所期望的样子——由请求头驱动的配置、程序化清理、无需建模平台特定的概念。
Caching is only useful if you can see what it's doing. The Workers Observability dashboard now surfaces cache hit information per invocation:
You can see, per Worker:
Cache hit ratio over time. The number you want trending up after you enable caching.
Hits, misses, updates, bypasses broken down. If your hit ratio is low, this is where you find out why — too many BYPASS responses (because something is setting a cookie?), too many MISS responses (because the cache key is partitioning more than you thought?), too many UPDATING responses (because max-age is shorter than your traffic interval?).
Because all of this lives on the same dashboard as your Worker's other observability — logs, exceptions, CPU time, request counts — you don't have to context-switch between looking at your zone and your Worker to understand what's happening.
缓存只有在你能够看到其运行状况时才有用。Workers 可观测性仪表板现在可以显示每次调用的缓存命中信息:
你可以按 Worker 查看:
随时间变化的缓存命中率。这是你在启用缓存后希望呈上升趋势的数字。
命中、未命中、更新、旁路的细分。如果你的命中率很低,这里可以让你找出原因——太多的 BYPASS 响应(因为某个东西设置了 cookie?),太多的 MISS 响应(因为缓存键的划分比你想象的更细?),太多的 UPDATING 响应(因为 max-age 比你的流量间隔短?)。
由于所有这些信息都与你的 Worker 的其他可观测性——日志、异常、CPU 时间、请求计数——位于同一个仪表板上,你无需在查看你的区域和你的 Worker 之间进行上下文切换即可了解发生了什么。
Cache hits don't run your Worker, and they don't bill CPU time. They do count as a request at the standard Workers request rate, the same as any other invocation. Cache misses and bypasses bill normally — request + CPU time, exactly as they would without caching.
Outcome Request charge CPU time charge Cache HIT (Worker does not run) Standard rate Not billed Cache MISS (Worker runs) Standard rate Billed Cache BYPASS (Worker runs) Standard rate Billed Static asset request Standard rate Not billed Worker-to-worker invocation Standard rate Billed if the Worker runs
There's no separate Workers Cache SKU and no per-GB cache storage fee. Tiered caching, purges, stale-while-revalidate, and the analytics described above are all included. If a request would have run your Worker and Workers Cache serves it as a hit instead, you still pay the standard request rate, but you pay no CPU time for that request. Because of this, that cache hit costs less than rendering the same response in your Worker.
One thing to watch: when caching is enabled, requests that are normally free — static asset requests and worker-to-worker invocations through service bindings or ctx.exports — are billed at the standard request rate, because each one now consults the cache in front of your Worker.
缓存命中不会运行你的 Worker,也不会计费 CPU 时间。但它们会按照标准的 Workers 请求费率计为一个请求,与任何其他调用相同。缓存未命中和旁路正常计费——请求 + CPU 时间,与没有缓存的计费方式完全相同。
结果 请求费用 CPU 时间费用 缓存命中(Worker 不运行) 标准费率 不计费 缓存未命中(Worker 运行) 标准费率 计费 缓存旁路(Worker 运行) 标准费率 计费 静态资产请求 标准费率 不计费 Worker 间调用 标准费率 如果 Worker 运行则计费
没有单独的 Workers Cache SKU,也没有按 GB 的缓存存储费用。分层缓存、清理、stale-while-revalidate 以及上述分析功能都已包含在内。如果一个请求本来会运行你的 Worker,但 Workers Cache 却以命中的方式为其提供了服务,你仍然需要支付标准的请求费率,但无需为该请求支付 CPU 时间。因此,这次缓存命中的成本低于在你的 Worker 中渲染相同响应。
需要注意的一点是:当缓存启用时,通常免费的请求——静态资产请求以及通过服务绑定或 ctx.exports 进行的 Worker 间调用——将按标准请求费率计费,因为每个请求现在都会查询位于你的 Worker 之前的缓存。
Things we know we want to do next:
Smarter co-location with Smart Placement. Today, Cloudflare chooses the upper-tier cache and Smart Placement target separately. On a full miss, the request may travel between Cloudflare locations twice: once to check the upper tier, and again to run your Worker near its data. We're working to coordinate those choices, so a miss only makes that long-distance trip once.
Larger response size limits. At launch, all responses follow the Free plan’s cacheable size limit (512 MB), regardless of your account. That’s temporary — the standard per-plan cache limits will apply once we finish a few rollout steps.
More framework integrations. Astro has built-in integration with Workers Cache. We’re working with maintainers to add similar integrations to other frameworks, including TanStack Start and Next.js via Vinext.
An API to mark cached responses stale. ctx.cache.purge() removes matching responses from cache. We’re looking at a ctx.cache.invalidate() API that makes matching responses behave as expired, so the next request can still get a fast stale response with stale-while-revalidate while your Worker refreshes the cache in the background.
Try it
Workers Cache is available today to every Worker on any plan.
To get started, add "cache": { "enabled": true } to your wrangler.jsonc, redeploy, and start setting Cache-Control headers. The Workers Cache documentation walks through the full feature surface — including the quickstart, cache keys, purging, composition patterns and examples, and debugging.
Workers used to run in front of the cache. Now they can also run behind it. Use whichever side you need — or, with service bindings, both at once.
We can't wait to see what you build.
我们接下来清楚要做的事情:
更智能地与 Smart Placement 协同定位。目前,Cloudflare 是分别选择上层缓存和 Smart Placement 目标的。在完全未命中时,请求可能会在 Cloudflare 的位置之间往返两次:一次检查上层缓存,另一次在数据附近运行你的 Worker。我们正在努力协调这些选择,使得一次未命中只需要进行一次长途旅行。
更大的响应体大小限制。发布时,所有响应都遵循免费计划的可缓存大小限制(512 MB),无论你的账户类型如何。这是暂时的——待我们完成几个发布步骤后,标准的分计划缓存限制将会生效。
更多框架集成。Astro 已内置了对 Workers Cache 的集成。我们正在与维护者合作,为其他框架添加类似的集成,包括通过 Vinext 集成的 TanStack Start 和 Next.js。
一个将缓存响应标记为过期的 API。ctx.cache.purge() 会从缓存中移除匹配的响应。我们正在考虑一个 ctx.cache.invalidate() API,它会使匹配的响应表现得像已过期一样,这样下一个请求仍然可以通过 stale-while-revalidate 快速获得过期的响应,同时你的 Worker 在后台刷新缓存。
立刻尝试
Workers Cache 今日起对任何套餐上的所有 Worker 开放。
要开始使用,将 "cache": { "enabled": true } 添加到你的 wrangler.jsonc 中,重新部署,然后开始设置 Cache-Control 请求头。Workers Cache 文档详细介绍了完整的功能面——包括快速入门、缓存键、清理、组合模式和示例,以及调试。
Worker 过去运行在缓存之前。现在它们也可以运行在缓存之后。使用你需要的任意一侧——或者通过服务绑定,同时使用两侧。
我们迫不及待想看到你的构建。