快取:`fetch()`、Cache API、Workers Cache 三層
這篇要解決的問題
Section titled “這篇要解決的問題”在 edge 上,快取比資料庫重要。第 8 篇的 KV 每次讀取要錢、第 9 篇的 D1 按 rows read 計費,而快取命中的成本趨近於零。任何一個嚴肅的 Workers 架構,快取都應該在資料層之前被設計,而不是之後才補。
問題是 2026 年的 Cloudflare 有三套獨立的快取機制,而且大部分 2025 年以前的教學只講其中一套 —— 那套現在官方已經建議新專案不要用了。
三件要先講清楚的事:
- 有第三套了。 Workers Cache(
[cache] enabled = true)在 2026 年 GA,官方文件直說「For new Workers, prefer Workers Caching」。這也解釋了第 3 篇 dumpctx時那個沒有文件的ctx.cache。 - Cache API 的一半方法是會 throw 的假貨。
caches.keys()、cache.add()這些存在於 prototype 上,typeof檢查會回"function",但一呼叫就the method is not implemented。 - Workers Cache 在本機完全不會動。 我實測 counter 一路遞增、沒有
cf-cache-statusheader、ctx.cache是undefined。
三層各自站在哪裡
Section titled “三層各自站在哪裡”這是最重要的一張圖。三套機制的差別不在功能,在位置:
┌──────────────────────────────────────┐ 使用者 ──────▶ │ ③ Workers Cache │ │ 在 Worker 前面。命中就不執行你的 │ │ 程式碼(省 CPU)。 │ └───────────────┬──────────────────────┘ │ MISS ▼ ┌──────────────────────────────────────┐ │ 你 的 W o r k e r │ │ │ │ ② Cache API(caches.default) │ │ 在 Worker 裡面。你自己 put/match。│ │ 每次請求都還是會執行 Worker。 │ └───────────────┬──────────────────────┘ │ fetch() ▼ ┌──────────────────────────────────────┐ │ ① fetch() 的 cf 選項 │ │ 在對外 subrequest 前面。省的是回 │ │ origin 的來回。 │ └──────────────────────────────────────┘官方對這個分工的說法很直接:
“a
fetch()subrequest hit saves a trip to your origin, while a Workers Caching hit saves your Worker from running at all.”
省的東西不一樣:fetch() 快取省 origin 往返、Cache API 省你自己重算的成本(但 Worker 照跑照計費)、Workers Cache 省整個 Worker 的執行。
① fetch() 的 cf 選項
Section titled “① fetch() 的 cf 選項”打對外請求時,把快取指令附在 fetch 上:
const res = await fetch(url, { cf: { cacheEverything: true, // 快取所有檔案類型,不只預設那些 cacheTtl: 300, // 強制 TTL,無視 origin header cacheTtlByStatus: { "200-299": 300, "404": 10, "500-599": 0 }, cacheTags: ["tenant:123"], // 之後可以按 tag purge cacheKey: `${tenantId}:${url}`, // 自訂快取鍵 },});完整選項:cacheEverything、cacheKey、cacheTags、cacheTtl、cacheTtlByStatus、vary、polish、webp、image、resolveOverride、scrapeShield、apps。
幾個要點:
cacheEverything/cacheTtl/cacheTtlByStatus只作用於 GET 和 HEAD。cacheTtlByStatus的負值代表「完全不要快取」。cf.cacheKey在 zone 層級可能是 Enterprise 限定 —— 官方兩個頁面說法不一致(/workers/examples/cache-using-fetch/說是 Enterprise,cf屬性參考頁沒標)。保守假設它是 Enterprise-only。- ⚠️ 需要 compatibility flag 才會覆蓋 Cache Rules。
request_cf_overrides_cache_rules自 compat date 2025-04-02 起預設開啟。沒開的話,你的cf設定會被靜默忽略,Cache Rules 生效而你完全不會知道。
② Cache API:低階、per-colo、要自己管
Section titled “② Cache API:低階、per-colo、要自己管”const cache = caches.default;const key = new Request(canonicalUrl, { method: "GET" });
const hit = await cache.match(key);if (hit) return hit;
const fresh = await expensiveWork();ctx.waitUntil(cache.put(key, fresh.clone())); // 注意 clonereturn fresh;實測本機就能跑:
$ for i in 1 2 3; do curl -s localhost:8787/cacheapi; echo; done{"layer":"cache-api","hit":false,"body":"expensive-value-1"}{"layer":"cache-api","hit":true,"body":"expensive-value-1"}{"layer":"cache-api","hit":true,"body":"expensive-value-1"}caches.open("ch06:custom") 的具名快取也能用,且與 caches.default 是獨立命名空間(caches.default 是和 fetch() 快取共用的那一份)。
📌 順帶更正一個舊說法:文件(Miniflare 2 時代)說
caches.open("default")是保留字會 throw。在現行 workerd 實測不會 throw。 別依賴這個行為,但也別以為它會擋你。
put() 的限制
Section titled “put() 的限制”會 throw:
request的 method 不是GETresponse的 status 是206 Partial Contentresponse帶Vary: *
會回 413:Cache-Control 指示不要快取,或 response 太大。
帶 Set-Cookie 的 response 永遠不會被快取 —— 除非你在 put() 前把那個 header 刪掉,或設 Cache-Control: private=Set-Cookie。
match() 會處理條件請求:帶 Range 且有 Content-Length → 回 206;If-None-Match 命中 ETag → 回 304。miss 則 resolve 成 undefined。
POST 不能直接當 key。 官方做法是把 body 雜湊成一個合成的 GET URL 當 key。
🔴 一半的方法是會 throw 的假貨
Section titled “🔴 一半的方法是會 throw 的假貨”caches 和 Cache 的 prototype 上有一整排 Web 標準方法:
$ curl -s localhost:8787/capabilities{ "cachesProto": ["open","delete","match","has","keys","constructor"], "cacheProto": ["add","addAll","delete","match","put","matchAll","keys","constructor"]}看起來 caches.keys()、cache.matchAll() 都在。實際呼叫:
$ curl -s localhost:8787/probe{ "caches.has": "THREW: Failed to execute 'has' on 'CacheStorage': the method is not implemented.", "caches.keys": "THREW: Failed to execute 'keys' on 'CacheStorage': the method is not implemented.", "caches.delete": "THREW: Failed to execute 'delete' on 'CacheStorage': the method is not implemented.", "cache.add": "THREW: Failed to execute 'add' on 'Cache': the method is not implemented.", "cache.addAll": "THREW: Failed to execute 'addAll' on 'Cache': the method is not implemented.", "cache.matchAll": "THREW: Failed to execute 'matchAll' on 'Cache': the method is not implemented.", "cache.keys": "THREW: Failed to execute 'keys' on 'Cache': the method is not implemented."}它們存在,但一律 throw。 這代表:
// ❌ Passes the check, throws at runtime.if (typeof caches.keys === "function") { const names = await caches.keys();}feature detection 完全無效。真正能用的只有 caches.default、caches.open()、cache.match()、cache.put()、cache.delete() 五個。
好消息是 wrangler types 產生的型別是正確的 —— CacheStorage 型別上根本沒有 has/keys/delete,Cache 上也沒有 add/addAll/matchAll/keys。要呼叫得先 cast。又一次:型別檔比文件可靠。
另外 match() 不支援 ignoreSearch 和 ignoreVary(只有 ignoreMethod)。
Cache API 的結構性限制
Section titled “Cache API 的結構性限制”官方自己列出來的三條,也是為什麼他們現在建議別用它:
- “It does not read through — responses are only cached when your Worker explicitly calls
put(), and every request still executes your Worker on the way in.”- “It does not collapse concurrent requests for the same resource. A burst of traffic to a fresh URL invokes your Worker once per request.”
- “It does not participate in tiered caching.”
第二條在實務上最痛:一個爆紅連結同時湧入 1000 個請求,就是 1000 次 Worker 執行 + 1000 次後端查詢,因為 match() 全部 miss,然後 1000 個 put() 互相覆蓋。這就是經典的 cache stampede,Cache API 不幫你擋。
第三條:Cache API 是單一資料中心本地的。put 只寫進當前 colo、delete 只刪當前 colo。要全球 purge 得走 zone 的 purge API。Tiered Cache 對它完全無效。
③ Workers Cache:新的預設選擇
Section titled “③ Workers Cache:新的預設選擇”2026 年 GA,需要 Wrangler ≥ 4.69.0:
{ "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2026-07-24", "cache": { "enabled": true, "cross_version_cache": false }}打開之後,它坐在你的 Worker 前面。命中時你的程式碼根本不執行。控制方式是你回應的 header:
return new Response(body, { headers: { "cache-control": "public, max-age=60, stale-while-revalidate=300" },});Header 優先序:cloudflare-cdn-cache-control > cdn-cache-control > Cache-Control。它支援 stale-while-revalidate 和 stale-if-error —— 這兩個 Cache API 不支援。
相對 Cache API 的四個結構性優勢:
- Read-through:不用自己
put()。 - 合併並發請求:爆紅連結只會打穿一次。
- 有 tiered cache:eyeball 附近的下層餵給跨網路的上層。
- 省 CPU 帳單:命中時 Worker 不執行,只計 request 不計 CPU-ms。
快取鍵包含:目標 entrypoint、path + query string(順序和尾斜線都有意義)、Worker 版本(除非開 cross_version_cache)、ctx.props。不包含 HTTP method、request host、request body —— 因為「A Worker is a zoneless entity」,快取屬於 Worker 而不是網域。
限制:
- 只有
GET/HEAD(兩者共用同一筆)。 520–526和206永不快取;response 帶Set-Cookie或 request 帶Authorization會 bypass。- 完全不作用於:Cron Triggers、Queue consumer、Workflows、Tail Workers、Durable Objects、自訂 RPC 方法、WebSocket upgrade。
- Purge 走
ctx.cache.purge()(by tag / by path prefix / purgeEverything),沒有 by-host,也沒有預熱 API。 - ⚠️ 上線初期所有帳號都套用 Free 方案的大小上限,不分方案。
🔴 Workers Cache 在本機完全不會動
Section titled “🔴 Workers Cache 在本機完全不會動”同樣的 Worker,cache.enabled = true,本機 wrangler dev 實測:
$ for i in 1 2 3; do curl -s localhost:8787/workerscache; done{"layer":"workers-cache","n":2}{"layer":"workers-cache","n":3}{"layer":"workers-cache","n":4}counter 一路遞增 —— 每次都執行了 Worker,一次都沒命中。沒有 cf-cache-status header,ctx.cache 的值是 undefined(雖然 "cache" in ctx 是 true)。
這是這一系列第四個「本機與 production 不一致」的案例,而且是影響最大的一個:你在本機完全無法驗證快取策略是否正確。
對照表:
wrangler dev | production | |
|---|---|---|
Cache API put/match | ✅ 正常運作 | ✅ |
caches.open() 具名快取 | ✅ | ✅ |
| Workers Cache | ❌ 完全無效 | ✅ |
fetch() 的 cf 快取選項 | ❌ 實質是 no-op(本機沒有 zone) | ✅ |
cf-cache-status header | ❌ 不存在 | ✅ |
規則:快取行為一律要在部署後用 cf-cache-status 驗證。 值有 MISS、HIT、BYPASS、DYNAMIC、UPDATING、REVALIDATED、EXPIRED、STALE。
| 情境 | 用 |
|---|---|
| 整頁 / 整個 API 回應可以被快取 | Workers Cache |
需要 stale-while-revalidate | Workers Cache |
| 流量會突然集中在單一 URL | Workers Cache(會合併並發) |
| 只想快取一次昂貴計算的中間結果 | Cache API |
| 快取鍵要依 request body 或自訂邏輯決定 | Cache API(合成 key) |
| 想省的是打 origin / 第三方 API 的來回 | fetch() 的 cf 選項 |
| 要跨 colo 一致 | 都不是 —— 用 KV(第 8 篇)或 DO(第 14 篇) |
一句話:新專案先開 Workers Cache,Cache API 留給它處理不了的細粒度情境。
| 項目 | Free | Paid |
|---|---|---|
| Cache API 單一物件大小 | 512 MB | 512 MB |
| Cache API 呼叫次數 / 請求 | 50 | 1,000 |
| Zone 快取物件大小 | 512 MB | 512 MB(Enterprise 5 GB) |
⚠️ Cache API 的呼叫次數和 fetch() 共用 subrequest 配額。 Free 方案 50 次是加總的 —— 一個迴圈裡 match() + fetch() + put() 很快就爆。
計費:
- Cache API 沒有獨立收費,但每次請求都還是會執行並計費 Worker。
- Workers Cache 也沒有獨立收費,命中仍以相同費率計 request,但 CPU time 只在 miss 或 bypass 時計費。所以它省的是 CPU-ms 不是 request 數。
動手做:三層並排
Section titled “動手做:三層並排”完整程式碼:
examples/ch06-caching/
cd examples/ch06-caching && npm install && npm run devB=localhost:8787
# 真正存在的 API 有哪些curl -s $B/capabilities | jq
# 哪些是會 throw 的假貨curl -s $B/probe | jq
# Cache API:第二次開始命中for i in 1 2 3; do curl -s $B/cacheapi; echo; done
# 具名快取for i in 1 2; do curl -s $B/named; echo; done
# Workers Cache:本機不會命中,counter 一路漲for i in 1 2 3; do curl -s $B/workerscache; echo; donecurl -s $B/counter部署後再跑一次 /workerscache,這次看 header:
curl -sD- https://<your-worker>.workers.dev/workerscache | grep -i cf-cache-status第一次 MISS,之後 HIT,而 n 會凍在同一個數字 —— 那就是 Worker 沒有被執行的證據。
接進 LinkForge
Section titled “接進 LinkForge”Redirect 熱路徑是整個系統唯一真正需要調快取的地方。
架構決策:redirect 走 Workers Cache,不走 Cache API。
{ "cache": { "enabled": true } }const target = await env.LINKS.get(slug);if (!target) return new Response("Not found", { status: 404 });
ctx.waitUntil(env.CLICKS.send({ slug, ts: Date.now() }));
return new Response(null, { status: 302, headers: { location: target, // 60s 新鮮 + 5 分鐘 stale-while-revalidate。 // 連結被改到時最多 60 秒不同步 —— 對短網址可以接受。 "cache-control": "public, max-age=60, stale-while-revalidate=300", },});四個理由:
① 省的是 CPU 不是請求數。 短網址是極端讀多寫少,同一個熱門 slug 一秒可能被打幾千次。Workers Cache 命中時 Worker 不執行 → 不消耗 CPU-ms、不打 KV。以第 42 篇的成本模型看,這是整個系統投報率最高的一個設定。
② 合併並發是關鍵。 一個連結在社群爆紅時,Cache API 會讓 1000 個並發請求變成 1000 次 KV 讀取;Workers Cache 只會穿透一次。
③ stale-while-revalidate 只有 Workers Cache 有。 KV 偶爾抖動時,使用者拿到的是 5 分鐘內的舊值而不是錯誤。
④ 但點擊統計會因此漏掉。 快取命中時 Worker 沒執行,ctx.waitUntil 也就沒發生。這是必須明講的取捨:
- 對「這個連結大概多熱門」的統計,60 秒粒度完全夠用。
- 對計費級的精確計數,第 14 篇的 Durable Object 才是答案。
- 兩者可以並存:cache 給趨勢、DO 給精確值。
本篇交付物:apps/redirector 加上 Workers Cache 設定與 cache-control 策略,以及一份記錄「為什麼接受統計不精確」的 ADR。
① 以為 Cache API 是唯一選擇
2026 年官方建議新專案用 Workers Cache。
② feature detection 檢查 caches.keys 之類的方法
它們存在但一律 throw。可用的只有 caches.default、caches.open、match、put、delete。
③ 在本機驗證快取策略
Workers Cache 本機完全不動,cf 選項也是 no-op。用部署後的 cf-cache-status。
④ 忘記 put() 前要 clone()
Response 的 body 是串流,讀掉就沒了 —— 和第 3 篇 passThroughOnException 是同一個道理。
⑤ 沒注意 Cache API 吃 subrequest 配額
Free 方案 50 次,和 fetch() 共用。
⑥ 以為 cf 選項一定會覆蓋 Cache Rules
需要 request_cf_overrides_cache_rules(compat date 2025-04-02 起預設)。Cache API 那邊還額外需要 cache_api_compat_flags。沒開就是靜默忽略。
⑦ 期待 Cache API 跨 colo
put/delete 都只作用於當前資料中心。
⑧ 快取了帶 Set-Cookie 的回應
不會成功。刪掉 header 或用 Cache-Control: private=Set-Cookie。
本篇要記住的三句話
Section titled “本篇要記住的三句話”- 三層省的東西不一樣:
fetch()省 origin 往返、Cache API 省重算、Workers Cache 省整個 Worker 執行。 - **Cache API 不 read-through、不合併並發、不參與 tiered cache。**這三條就是官方轉推 Workers Cache 的原因。
- **快取在本機驗不了。**Workers Cache 完全不動、
cf選項是 no-op —— 一律用部署後的cf-cache-status。
- Workers Cache(新,先讀這個)與 限制
- Cache API 參考
- How the Cache works
- Workers 與 Cache Rules 的互動(compat flag 那段很重要)
cf屬性完整列表
下一篇:07. Static Assets 與全端 Worker —— assets 的五個欄位、SPA 白畫面的真正原因,以及 Pages 現在到底該不該用。