跳到內容

快取:`fetch()`、Cache API、Workers Cache 三層

查證日期
驗證環境wrangler@4.114.0·workerd@1.20260722.1·compatibility_date: 2026-07-24

在 edge 上,快取比資料庫重要。第 8 篇的 KV 每次讀取要錢、第 9 篇的 D1 按 rows read 計費,而快取命中的成本趨近於零。任何一個嚴肅的 Workers 架構,快取都應該在資料層之前被設計,而不是之後才補。

問題是 2026 年的 Cloudflare 有三套獨立的快取機制,而且大部分 2025 年以前的教學只講其中一套 —— 那套現在官方已經建議新專案不要用了。

三件要先講清楚的事:

  1. 有第三套了。 Workers Cache([cache] enabled = true)在 2026 年 GA,官方文件直說「For new Workers, prefer Workers Caching」。這也解釋了第 3 篇 dump ctx 時那個沒有文件的 ctx.cache
  2. Cache API 的一半方法是會 throw 的假貨。 caches.keys()cache.add() 這些存在於 prototype 上typeof 檢查會回 "function",但一呼叫就 the method is not implemented
  3. Workers Cache 在本機完全不會動。 我實測 counter 一路遞增、沒有 cf-cache-status header、ctx.cacheundefined

這是最重要的一張圖。三套機制的差別不在功能,在位置

┌──────────────────────────────────────┐
使用者 ──────▶ │ ③ 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 上:

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}`, // 自訂快取鍵
},
});

完整選項:cacheEverythingcacheKeycacheTagscacheTtlcacheTtlByStatusvarypolishwebpimageresolveOverridescrapeShieldapps

幾個要點:

  • 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())); // 注意 clone
return fresh;

實測本機就能跑:

Terminal window
$ 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。 別依賴這個行為,但也別以為它會擋你。

會 throw:

  • request 的 method 不是 GET
  • response 的 status 是 206 Partial Content
  • responseVary: *

會回 413:Cache-Control 指示不要快取,或 response 太大。

Set-Cookie 的 response 永遠不會被快取 —— 除非你在 put() 前把那個 header 刪掉,或設 Cache-Control: private=Set-Cookie

match() 會處理條件請求:帶 Range 且有 Content-Length → 回 206If-None-Match 命中 ETag → 回 304。miss 則 resolve 成 undefined

POST 不能直接當 key。 官方做法是把 body 雜湊成一個合成的 GET URL 當 key。

🔴 一半的方法是會 throw 的假貨

Section titled “🔴 一半的方法是會 throw 的假貨”

cachesCache 的 prototype 上有一整排 Web 標準方法:

Terminal window
$ 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() 都在。實際呼叫:

Terminal window
$ 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.defaultcaches.open()cache.match()cache.put()cache.delete() 五個。

好消息是 wrangler types 產生的型別是正確的 —— CacheStorage 型別上根本沒有 has/keys/deleteCache 上也沒有 add/addAll/matchAll/keys。要呼叫得先 cast。又一次:型別檔比文件可靠。

另外 match() 不支援 ignoreSearchignoreVary(只有 ignoreMethod)。

官方自己列出來的三條,也是為什麼他們現在建議別用它:

  • “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 對它完全無效。

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-revalidatestale-if-error —— 這兩個 Cache API 不支援。

相對 Cache API 的四個結構性優勢:

  1. Read-through:不用自己 put()
  2. 合併並發請求:爆紅連結只會打穿一次。
  3. 有 tiered cache:eyeball 附近的下層餵給跨網路的上層。
  4. 省 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(兩者共用同一筆)。
  • 520526206 永不快取;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 實測:

Terminal window
$ 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 ctxtrue)。

這是這一系列第四個「本機與 production 不一致」的案例,而且是影響最大的一個:你在本機完全無法驗證快取策略是否正確。

對照表:

wrangler devproduction
Cache API put/match✅ 正常運作
caches.open() 具名快取
Workers Cache完全無效
fetch()cf 快取選項❌ 實質是 no-op(本機沒有 zone)
cf-cache-status header❌ 不存在

規則:快取行為一律要在部署後用 cf-cache-status 驗證。 值有 MISSHITBYPASSDYNAMICUPDATINGREVALIDATEDEXPIREDSTALE

情境
整頁 / 整個 API 回應可以被快取Workers Cache
需要 stale-while-revalidateWorkers Cache
流量會突然集中在單一 URLWorkers Cache(會合併並發)
只想快取一次昂貴計算的中間結果Cache API
快取鍵要依 request body 或自訂邏輯決定Cache API(合成 key)
想省的是打 origin / 第三方 API 的來回fetch()cf 選項
要跨 colo 一致都不是 —— 用 KV(第 8 篇)或 DO(第 14 篇)

一句話:新專案先開 Workers Cache,Cache API 留給它處理不了的細粒度情境。

項目FreePaid
Cache API 單一物件大小512 MB512 MB
Cache API 呼叫次數 / 請求501,000
Zone 快取物件大小512 MB512 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 數。

完整程式碼:examples/ch06-caching/

Terminal window
cd examples/ch06-caching && npm install && npm run dev
Terminal window
B=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; done
curl -s $B/counter

部署後再跑一次 /workerscache,這次看 header:

Terminal window
curl -sD- https://<your-worker>.workers.dev/workerscache | grep -i cf-cache-status

第一次 MISS,之後 HIT,而 n 會凍在同一個數字 —— 那就是 Worker 沒有被執行的證據。


Redirect 熱路徑是整個系統唯一真正需要調快取的地方。

架構決策:redirect 走 Workers Cache,不走 Cache API。

apps/redirector/wrangler.jsonc
{ "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.defaultcaches.openmatchputdelete

③ 在本機驗證快取策略

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


  1. 三層省的東西不一樣fetch() 省 origin 往返、Cache API 省重算、Workers Cache 省整個 Worker 執行
  2. **Cache API 不 read-through、不合併並發、不參與 tiered cache。**這三條就是官方轉推 Workers Cache 的原因。
  3. **快取在本機驗不了。**Workers Cache 完全不動、cf 選項是 no-op —— 一律用部署後的 cf-cache-status


下一篇07. Static Assets 與全端 Worker —— assets 的五個欄位、SPA 白畫面的真正原因,以及 Pages 現在到底該不該用。