产品发布 · 小互解读

Cloudflare 发布 Workers Cache:缓存前置到每个 Worker,命中不计 CPU 费

一行 Wrangler 配置加标准 HTTP 缓存头,缓存还能嵌进 Worker 内部任意入口之间。
来源 · Cloudflare Blog 阅读 · 7 分钟 Cloudflare Workers · 边缘缓存 · Serverless
三十秒速览
  • Cloudflare 今天发布 Workers Cache,把区域分级缓存直接接在每个 Worker 前面,一行 Wrangler 配置即可开启。
  • 缓存命中时 Worker 完全不运行,也不产生 CPU 计费;未命中才执行代码,并回填上下两层缓存。
  • 缓存不再挂在域名(zone)上,而是跟着 Worker 走:workers.dev、预览环境、Workers for Platforms 都能用,清缓存也只影响自己。
  • 最大的突破是缓存能插进同一个 Worker 内部任意入口之间,配合 ctx.props 还能让登录用户的接口安全共享缓存。
  • 今天起对所有套餐的 Worker 开放,没有单独产品,也没有额外收费项目。
立场提示:本文编译自 Cloudflare 官方发布博客。文中「命中零 CPU 计费」「约 50ms 覆盖全球 95% 网民」「没见过其他平台/CDN 能做到」等表述,均为厂商自述口径。
1 是什么

Worker 自己也能有一层缓存了

Cloudflare 今天推出 Workers Cache,一层直接接在 Worker 前面的缓存。开启它只要在配置里加一行,控制它只要用你早就熟悉的标准 HTTP 缓存头。

缓存命中时,你的 Worker 完全不运行,这次请求也不产生 CPU 计费。只有没命中,代码才跑一次,跑完顺手把结果存进缓存。下一次请求,从地球上任何地方发来,都能直接读缓存。

为什么值得看:按 Cloudflare 官方表述,目前没见过其他平台能把缓存嵌进单个部署单元内部、按每个入口单独开关,也没见过其他 CDN 把「已登录用户 API 安全共享缓存」做成内置能力。

开启缓存,全部配置就这一段
{
  "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 要配,没有规则引擎要搭,没有单独的缓存要开通,也没有第二个产品要登录。Worker 的代码就是配置面,缓存跟着 Worker 跑到哪算哪:自定义域名、workers.dev、service binding 后面、预览环境、Workers for Platforms 租户里,都是一个 Worker,一份缓存,配一次。

0
缓存命中时 Worker 消耗并计费的 CPU 时间
~50ms
Cloudflare 网络覆盖全球约 95% 网民所需的最大延迟半径
512MB
目前所有套餐统一沿用的可缓存响应体积上限(后续按套餐区分)
2 背景

Worker 原来站在缓存前面,现在自己就是源站

2017 年 Cloudflare 刚推出 Workers 时,卖点是让你把代码跑在网络边缘,在请求奔向源站的路上把它改一改。那时 Worker 站在缓存和源站的前面。

源站,就是真正存着你网站内容、负责生成页面的那台服务器。Worker 站在它前面时,你可以加请求头、重写网址、做 A/B 分流、在流量到达源站之前先过滤一遍。对当时那批场景,这个位置是对的,你能完全掌控什么该缓存、什么不该。

后来情况变了。Astro、TanStack Start、Next.js、Remix、SvelteKit 这些框架都提供 Cloudflare 适配,直接把整个应用打包成一个 Worker。它们背后没有源站,Worker 本身就是那台服务器。

2017 · 旧架构
请求 Worker 缓存 源站
Worker 在最前面拦截流量,缓存挡在它身后的源站前。
现在 · Workers Cache
请求 缓存 Worker(就是源站)
缓存挡在最前面,命中就返回,Worker 一次都不跑。

当 Worker 就是源站,老架构里缓存挡在它身后,就没什么可缓存了。每个请求都得把代码重跑一遍,哪怕这次返回的内容和一秒前一模一样。Workers 的运行时快到足以每个请求都现渲染,一秒钟几千万个请求都不喘气;但「足够快到每次都渲染」仍要付两笔账:每次页面加载的延迟,和每次执行的 CPU 时间。而对服务端渲染的应用,每次页面加载按定义就是一次渲染。

过去你只能在两个都不痛快的选项里挑一个:

构建时全量预渲染
页面加载快,但每改一次都要整站重建重部署。几千页的文档站要跑 5 到 10 分钟,大型电商更糟,而且你碰任何东西它都要重跑一遍。
每个请求都现渲染
内容永远最新,但每次页面加载都付渲染成本,每个访客都付延迟。

Workers Cache 给了第三条路:按需在服务端渲染,把渲染结果缓存住,按你选的存活时间刷新。新页面第一个请求照样渲染,之后到缓存过期为止,每个请求都像访问静态页一样被端出来。你拿到静态站的速度,省掉了构建时间;也拿到服务端渲染的新鲜度,省掉了持续成本。

3 新架构

缓存反过来,挡在 Worker 前面

Workers Cache 默认就是区域分级的两层缓存,而且这层拓扑全自动生效,你不用配任何东西。请求进来先查哪一层、什么时候才真的惊动你的代码,是这样走的:

请求进来 下层缓存 离用户最近的那个机房 命中 直接返回 未命中 上层缓存 全网汇总,数量更少 命中 返回并回填下层 两层都没有 跑 Worker 一次 这一个请求才真的现渲染 把结果同时写回上层和下层缓存 全球第一个请求就把上层缓存填好 之后任何机房的请求都能直接命中,哪怕它从没见过
两层区域缓存:下层就近,上层汇总。第一个请求填满上层后,全网复用。
打个比方

像连锁便利店进货:先翻自己店里的货架(下层缓存),没有再问区域仓库调货(上层缓存),仓库也没有,才找厂家现做(跑 Worker)。厂家做好这一批,货架和仓库都补上,下次谁来都不用再麻烦厂家。

这套拓扑和今天给 zone 用的 Tiered Cache 是同一套,区别在于你不用去配它,没有「给我的 Worker 打开分级缓存」这种开关。只要 Worker 开了缓存,分级就白送。如果你的 Worker 用了 Smart Placement,缓存也和它干净地叠在一起:先查两层缓存,两层都没命中,Smart Placement 才把执行调度到离数据近的地方。

4 关键机制

缓存过期的那一下,没人在干等

stale-while-revalidate 直译是「陈旧时重新验证」。它告诉 Cloudflare:缓存过期后,允许先把旧的那份立刻端给用户看,同时在后台悄悄重新跑一遍 Worker 换成新的。就是这一条,把「我们帮你缓存 Worker」变成了「你的 Worker 站点感觉像静态站」。

没有它,缓存过期后的第一个请求得等 Worker 从头渲染,那点延迟用户是能感觉到的。有了它,过期后第一个请求立刻拿到旧页面(带一个 Cf-Cache-Status: UPDATING 头),Worker 在后台跑着回填缓存。包括触发这次刷新的那个人在内,每个用户拿到的都是缓存速度的响应。

缓存刚生成时间往右 →越来越旧
新鲜期
max-age 之内
Cloudflare 直接返回缓存。
Worker 不跑
陈旧期
stale-while-revalidate 之内
先把旧内容立刻端给用户,Worker 在后台跑着刷新。谁都不用等。
Worker 后台跑
双过期
两个窗口都过了
Cloudflare 现跑一次 Worker 生成新响应,只有这一个请求要等这次渲染。
Worker 当场跑

窗口由你定。看你的内容多久变一次,就配多长:

几分钟变一次的商品目录
max-age=300
stale-while-revalidate=3600
访客基本不用等,Worker 又跑得够勤,内容保持新鲜。
几乎不变的博客归档
max-age=86400
stale-while-revalidate=2592000
每一页每天只跑一次 Worker。

只有一个全新页面的第一个请求,才付一次完整渲染成本。之后对访客来说,这页的表现就和静态输出一样,而页面怎么生成,仍然由你的 Worker 说了算。

5 概念转变

这层缓存跟着 Worker 走,不跟着域名走

Cloudflare 一直有缓存,但它是按 zone(域名的配置单元)来配的:Cache Rules、Page Rules、缓存文件扩展名列表、Cache Reserve、分级缓存拓扑、自定义缓存键,全都设在 zone 上。过去一个 Worker 要么去迁就所在 zone 的配置,要么想办法绕开它。

Workers Cache 换了个归属:它是你这个 Worker 的缓存,归 Worker 所有,不归某个 zone。这带来几个实打实的好处:

  • 没有 zone 配置要管。Cache Rules、缓存级别设置、文件扩展名列表、Page Rules,对 Workers Cache 统统不适用。Worker 的 Cache-Control 头就是全部配置。
  • 缓存跟着 Worker,不跟着主机名。一个 Worker 同时绑在 api.example.com、api.example.net,又通过 service binding 被调用,这三条路共享同一份缓存。请求 /users/42,无论从哪条进来,命中的都是同一个缓存条目。
  • workers.dev 也能用。预览环境各自有独立缓存,测一个改动不会污染生产;Workers for Platforms 里每个租户 Worker 各自缓存,互不干扰。这些位置过去在缓存上都是二等公民,现在不是了。
  • 清缓存只作用于自己这个入口。你调 purge 清空,只清你这个 Worker 入口的缓存,不会误伤 zone 里别的内容,也不会一个 Worker 的部署把另一个的数据清掉。

凡是你想配的缓存行为,都写在代码里:哪些路径给更长的存活时间,就按路径分支设不同的 max-age;哪些请求绕过缓存,就返回 Cache-Control: private;缓存键怎么算,就控制往 ctx.props 里放什么、在网关 Worker 里先把网址归一化。你已经写好的 Worker,就是配置面。

6 最大突破

缓存能塞进一个 Worker 内部的任意接缝里

Cloudflare 认为的最大突破

Workers Cache 挡在每一个 Worker 入口前面:默认导出的入口、每个具名入口,以及同一个 Worker 内部一个入口通过 ctx.exports 调用另一个入口的那一次调用。最后这一句,改变了你能搭出什么东西。

一个 Worker 程序内部可以拆成好几个独立的「子入口」(entrypoint),写代码时直接互相调用,不用真的发一次网络请求出去再绕回来。当一个入口通过 ctx.exports 调另一个入口时,缓存会像对待浏览器请求一样评估这次调用。

打个比方

像一家公司里,前台(网关入口)接待完访客,直接把需求转给内部对应科室(另一个入口)处理,不用访客自己再跑到街对面另一栋楼。缓存就夹在前台和科室之间:科室上次的答复还在,前台直接拿来给访客,科室这次连门都不用开。

命中就返回缓存,被调的入口一次都不跑;没命中才跑一次,并按自己的入口、路径、查询串、ctx.props 存一份缓存。调用方每次都跑,但它甩给被调方的活儿,被独立记了缓存。你按入口逐个决定谁缓存:wrangler 的 exports 里给每个入口单独开关。网关、路由这类做认证、归一化、分发的入口,应该关掉缓存,让它每次都跑,它自己的输出也永远不会被当缓存端出去。

一个 Worker · 一个源文件 · 一次部署 网关入口 default · 认证 · 路由 缓存:关 缓存开关 命中直接返回 内部计算入口 重活 · 命中则不跑 缓存:开 缓存是写在代码里的一层,不是外挂的产品
招牌图:缓存像一把嵌在调用链中间的开关,按入口逐个点亮或熄灭。

缓存怎么配,全表达成普通的 Worker 代码:你调哪个入口、转发什么请求、传什么 ctx.props、设什么 Cache-Control。下面这个 Worker 一口气做了三件在别的平台不好一起做的事:每个请求都认证、把昂贵的后端缓存在一个多租户安全的缓存键后面、数据变了就清掉那份缓存。缓存按入口配置,网关每次都得跑(既为认证,也因为缓存了网关就会跳过这道认证),所以默认入口关缓存,只给内部入口开:

wrangler 里按入口开关:网关关,内部计算入口开
{
  "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 } }
  }
}
点开看这一个 Worker 的完整代码(网关 + 缓存后端 + 按标签失效)
src/index.ts · 一个源文件里的两个执行阶段,缓存夹在中间
import { WorkerEntrypoint } from "cloudflare:workers";

interface Env { API_TOKEN: string; }
interface Props { userId: string; }

// 内部入口:昂贵的活儿。缓存挡在它前面,命中时这段代码根本不跑。
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 就给网关关缓存、给后端开缓存,缓存夹在它们中间,按用户分键、被写路径按标签清掉、后台刷新时端旧内容。这个缓存阶段不是外挂上去的,它是程序的一层,写在代码里。

同样的形状还能套在别处:把 Durable Object 包在一个入口后面,读命中时就不碰它、写直接进 Durable Object 再按标签清缓存;在网关里把追踪参数(?utm_source=…)剥掉再转发,让缓存只看到干净的网址,各种带参链接收敛成同一个缓存条目。这些入口可以一层层叠起来,每两层之间的缓存阶段,都不是你去配的,只是你决定把它放在哪。

7 多租户安全

登录用户的接口,也能安全地共用缓存

如果你缓存的是按用户返回不同内容的接口,比如给每个登录用户返回各自数据的 API,你得保证一个用户永远看不到另一个用户的缓存。老办法是「认证过的请求一律不缓存」,Cloudflare 对带 Authorization 头的请求默认就这么绕过。但「什么都不缓存」等于把整个性能收益丢掉。

Workers Cache 的解法是把调用方的 ctx.props 也算进缓存键里。给每次缓存请求贴上一个隐形的「这是哪个用户」标签,同一个网址,不同用户各自存各自的缓存副本,A 的数据绝不会被当缓存返回给 B。

用户 A → /me 用户 B → /me 网关认证 剥掉 Authorization 把 userId 塞进 ctx.props 缓存条目 · userId=A 缓存条目 · userId=B 同一个 /me,各存各的,不串号
ctx.props 把「这是谁」写进缓存键,多租户隔离由缓存键自己搞定。
被调后端:ctx.props.userId 进缓存键,每个用户各存各的
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,就变成了「按用户缓存、还带完整安全隔离」,隔离这件事交给缓存键替你做了。按 Cloudflare 说法,别的 CDN 会逼你在「正确」和「命中率」之间二选一,而它没见过第二家把这个做成内置能力。

顺带拼出「离用户近 + 离数据近」的两级架构

Web 性能里有个长期矛盾:你想让代码跑得离用户近(用户到服务器的往返在关键路径上),又想让它跑得离数据近(每次查库也是一次往返)。挑一个,另一个就慢。Cloudflare 网络本身已经能在约 50ms 内覆盖全球约 95% 的网民;配合 Smart Placement,代码又能贴着数据跑。缺的那块拼图,正是这层缓存。

用户 Worker A · 挨着用户 认证 · 路由 · 渲染外壳 缓存B 的缓存 Worker B · 挨着数据 查库 · 渲染 · 重活 命中:用户 → A → B 的缓存命中 → 返回,压根不跳到 B
缓存当接缝:热页面在 A 直接命中返回,数据跳只在没命中时才付。

你不用特意设计什么架构就能吃到这个:把应用写成两个 Worker,用 service binding 把一个指向另一个,在 Worker B 的 wrangler 里把缓存打开,就行了。

8 内容协商 · 监控 · 账单

内容协商、监控看板和账单怎么算

真实应用很少给每个客户端返回一模一样的字节。同一个商品页,浏览器要 HTML,API 客户端要 JSON;同一张图,支持的客户端要 WebP,不支持的要 JPEG;同一个首页,可能按用户返回中文、法文或日文。

一个网址存多份:Vary 头

没有缓存时这很好办,Worker 读一下请求头返回对应的东西就行;难在带上缓存。多数缓存只给你两个糟糕选项:有多版本的网址干脆不缓存,或者只缓存一份端给所有人。Workers Cache 支持标准的 Vary 头:你返回 Vary: Accept,Cloudflare 就按 Accept 的不同取值各存一份变体,只把匹配进来请求的那一份返回。一个网址,两份缓存变体,Worker 第一次给每一份各写一次,之后对谁都零次运行。它对 Vary 的头没有白名单,你列什么都行,Cloudflare 按原样取值分变体。

缓存和 Worker 在同一块看板上

Workers 的 Observability 看板现在按每次调用显示缓存信息。你能按 Worker 看到命中率随时间的走势,以及命中(HIT)、未命中(MISS)、更新(UPDATING)、绕过(BYPASS)各自的明细。命中率低的时候,这里就是你查原因的地方:BYPASS 太多,可能有东西在设 cookie;MISS 太多,可能缓存键分得比你以为的更碎;UPDATING 太多,可能 max-age 比你的流量间隔还短。这些和 Worker 的日志、异常、CPU 时间、请求数在同一块看板上,不用来回切换。

账单:命中不算 CPU,但有两项开始计标准请求费

缓存命中不跑 Worker,也不计 CPU 时间,但它照样算一次请求,按标准 Workers 请求费率计,和任何一次调用一样。未命中和绕过按正常算:请求费加 CPU 时间。

场景请求费CPU 时间费
缓存命中 HIT(Worker 不跑)标准费率不计
缓存未命中 MISS(Worker 跑)标准费率
缓存绕过 BYPASS(Worker 跑)标准费率
静态资源请求标准费率不计
Worker 间调用标准费率跑了才计

没有单独的 Workers Cache 收费项,也没有按 GB 的缓存存储费。分级缓存、清缓存、stale-while-revalidate、上面那些分析,全包含在内。一次本来会跑 Worker 的请求,被当命中端出去,你仍付标准请求费,但这次不付 CPU 时间,所以这一次比在 Worker 里现渲染更便宜。要留意一点:开了缓存后,本来免费的静态资源请求和通过 service binding 或 ctx.exports 的 Worker 间调用,会按标准请求费率计,因为每一次现在都要先查一遍前面的缓存。

9 上手 · 路线图

今天就能开,官方还在往下修

Workers Cache 今天起对所有套餐的每个 Worker 开放,在 Wrangler 里启用。上手就三步:给 wrangler.jsonc 加上 "cache": { "enabled": true },重新部署,然后开始在响应上设 Cache-Control 头。

官方也列了接下来还没做完的几件事:

  • Smart Placement 和缓存更好地协同。现在上层缓存和 Smart Placement 目标是分开选的,完全没命中时,请求可能在 Cloudflare 机房之间跑两趟:一趟查上层缓存,一趟跑近数据的 Worker。他们在协调这两个选择,让一次未命中只跑一趟远距离。
  • 放开体积上限。发布时所有响应统一沿用免费套餐的 512MB 可缓存上限,不分账户。这是临时的,走完几步 rollout 后会按套餐区分。
  • 更多框架适配。Astro 已经内置 Workers Cache 适配,服务端渲染页面自动走「渲染一次、缓存、后台刷新」的流程;TanStack Start、Next.js(经由 Vinext)等也在跟进。
  • 新增 ctx.cache.invalidate()。purge 是直接把匹配的响应从缓存里删掉,invalidate 会让它们变成「已过期」状态,这样下一个请求还能靠 stale-while-revalidate 先拿到快速的旧内容,Worker 在后台刷新。
Worker 过去跑在缓存前面,现在也能跑在缓存后面。用你需要的那一边,或者靠 service binding,两边同时用。 , Cloudflare Blog《Your Worker can now have its own cache in front of it》
来源:The Cloudflare Blog,2026 年 7 月 6 日发布,原文《Your Worker can now have its own cache in front of it》(blog.cloudflare.com/workers-cache/)。本文为厂商发布内容的中文解读,代码示例与数据均引自原文,文中架构图、流程图、时间轴均为解读站据原文描述绘制。命中零 CPU 计费、约 50ms 覆盖 95% 网民、「没见过其他平台/CDN 能做到」等表述为 Cloudflare 官方口径。