結論
Cloudflare Workers の fetch オプションで cf: { cacheEverything: false } を渡しても、エッジキャッシュを bypass する指定にはなりません。Cloudflare cache を bypass する意図を表す標準的な手段は cache: 'no-store' です。今回は、ただのドジだった話。
// ✗ NG: bypass にならない
await fetch(req, { cf: { cacheEverything: false } });
// ✗ NG: TTL を 0 にするだけで bypass の標準手段ではない
await fetch(req, { cf: { cacheTtl: 0 } });
// ✓ OK: 公式に bypass 標準手段として明示されている
await fetch(req, { cache: 'no-store' });
公式ドキュメント (Workers fetch) には次のように書かれています。
Only
cache: 'no-store'andcache: 'no-cache'are supported. Any other cache header will result in aTypeError.When
cache: 'no-store', requests to origins not hosted by Cloudflare bypass the use of Cloudflare's caches.
| option | 役割 | bypass か |
|---|---|---|
cf.cacheEverything |
通常はキャッシュしないレスポンスもキャッシュ対象にする | いいえ |
cf.cacheTtl / cf.cacheTtlByStatus
|
キャッシュ TTL を上書きする | いいえ |
cache: 'no-store' |
Cloudflare cache の利用を bypass する意図を表す | はい |
cf.cacheKey |
キャッシュキーを変える | いいえ |
何を誤解したか
cacheEverything: false という値は、名前だけ見ると「キャッシュを無効にする」と読めます。実際には 「すべてをキャッシュする(true)」のオン / オフを切り替えるフラグ であって、bypass を意味しません。
公式ドキュメント (Cache using fetch) を整理すると、次のようになります。
-
cacheEverything: true→ Cloudflare がデフォルトでキャッシュしないファイル(HTML 等)も強制的にキャッシュさせる -
cacheEverything: false→ デフォルトのキャッシュ判定に戻す。HTML を強制的にキャッシュする指定ではないが、通常の条件でキャッシュされるものは残る
「true でキャッシュ拡張する」という設計なので、false はゼロの状態であって bypass ではありません。
どこで遠回りしたか
cacheTtl は「Cloudflare がオリジンから再検証する前にリソースをキャッシュしておく秒数」を指定する TTL override の API です。0 を指定すると保存期間を 0 秒にする方向の制御にはなりますが、これはあくまで TTL の制御で、cache: 'no-store' のように「Cloudflare cache を使わない」という意図を直接表す API ではありません。
公式ドキュメントで「bypass」と明示されているのは cache: 'no-store' だけなので、意図を素直にコードに表したいならこちらを選ぶのが安全です。
確認が必要だったこと
ダッシュボード側で「HTML もキャッシュする」Cache Rules を設定している場合、Workers の cache 設定が override します。ただしこれには compatibility flag が必要で、API ごとにフラグ名が違います。
How Workers interact with Cache Rules によれば、次のとおりです。
- Fetch API 用:
request_cf_overrides_cache_rules - Cache API 用:
cache_api_request_cf_overrides_cache_rules
If the correct flag is not enabled for the API you are using, your Worker's cache settings are silently ignored.
フラグが未有効だと Workers の cache 設定がサイレントに無視されます。これは原因究明が難しいので、新規プロジェクトでは compatibility date を 2025-04-02 以降に揃え、Fetch API のフラグがデフォルト有効になる状態で書き始めるのが無難です。
本記事で扱うのは Fetch API (fetch(req, { cf: ... })) の話です。Cache API (caches.default.put() / match()) には別の compatibility flag と前提条件があるため、ここでは深追いしません。
| compatibility date | Fetch API のフラグ |
|---|---|
| ≥ 2025-04-02 | デフォルト有効 |
| < 2025-04-02 | 手動設定が必要 |
# wrangler.toml
compatibility_date = "2025-04-02"
古い compatibility date を維持するなら、手動で flag を入れます。
compatibility_date = "2024-12-01"
compatibility_flags = ["request_cf_overrides_cache_rules"]
対応方法
方法 1: cache: 'no-store'
await fetch(req, { cache: 'no-store' });
公式に bypass 標準手段として明示されている API です。意図がコードから読み取りやすいです。
注意点として、Cloudflare Workers の型定義 (@cloudflare/workers-types) の RequestInit には cache フィールドが含まれていない場合がある。まず型定義を更新し、それでも通らない場合は @ts-expect-error で局所的に抑制するか、プロジェクト側で RequestInit の型を補強する。
方法 2: cacheTtl: 0 と cacheTtlByStatus
TTL を 0 にして保存期間を極端に短くする方法です。上述のとおり公式が「bypass 標準」と呼んでいる API ではありません。複合的なキャッシュ制御が必要な場面で使います。
await fetch(req, {
cf: {
cacheTtlByStatus: { '200-299': 0, '300-399': 0, '400-599': 0 },
},
});
方法 3: cacheKey を一意化
「同じ URL でもユーザーごとに別キャッシュにしたい」場合は cacheKey を一意化します。これは bypass ではなく「キャッシュ対象のキー設計」を変える方法です。
await fetch(req, {
cf: {
cacheKey: `${req.url}#user=${userId}`,
},
});
方法 4: response 側で Cache-Control: no-store
origin から返るレスポンスの Cache-Control ヘッダで制御する方法です。Worker 側ではなく origin の責任にする設計です。
no-store と no-cache の違い
Workers fetch では cache: 'no-store' と cache: 'no-cache' がサポートされています。no-store は Cloudflare cache の利用を bypass する指定で、no-cache は origin への再検証を強制する指定です。
「キャッシュを使わず毎回 origin へ取りに行きたい」という意図なら no-store、「キャッシュは使ってもよいが origin に確認してから返したい」という意図なら no-cache と考えると整理しやすいです。
検証時の注意点
cf-cache-status レスポンスヘッダを見るのが王道ですが、値の意味は文脈依存なのでテストの期待値として固定するときは慎重に見た方がよいです。
curl -I https://example.com/page
# cf-cache-status: HIT ← キャッシュから返している
# cf-cache-status: MISS ← キャッシュ対象だが今回は MISS(次から HIT になる)
# cf-cache-status: BYPASS ← origin Cache-Control や認可条件で bypass
# cf-cache-status: DYNAMIC ← Cloudflare がデフォルトでキャッシュしないと判断
Cache responses を読むと、BYPASS は主に「origin Cache-Control や cookie / authorization 等の条件で bypass された」状態を指しています。一方 Workers の cache: 'no-store' で発生する bypass は別経路で、main request なのか subrequest なのかによっても見え方が変わります。
そのため、「bypass を期待するテスト」を cf-cache-status だけで固定するのは避けたほうがよいです。MISS でも「今回は origin に到達したが、キャッシュ対象として扱われた」状態なので、bypass の検証としては弱いです。確実に確認したいなら、同じ URL を 2 回叩いたときに毎回 origin へ到達していることを、origin 側のデバッグヘッダやカウンタで検証するのが安全です。
it('bypass フラグ付きリクエストは毎回 origin に到達する', async () => {
const r1 = await fetch('https://example.com/page', {
headers: { 'X-Cache-Bypass-Token': BYPASS_SECRET },
});
const r2 = await fetch('https://example.com/page', {
headers: { 'X-Cache-Bypass-Token': BYPASS_SECRET },
});
// origin が毎回異なる値を返すデバッグヘッダを前提にする
expect(r1.headers.get('x-origin-request-id')).not.toBeNull();
expect(r2.headers.get('x-origin-request-id')).not.toBeNull();
expect(r1.headers.get('x-origin-request-id')).not.toBe(
r2.headers.get('x-origin-request-id'),
);
});
どこがドジだったか
実コードベースで「キャッシュ bypass 機能を実装する PR」をレビューしていたとき、最初は cacheEverything: false を見て、「false ならキャッシュしないはずだ」と思っていました。
次に、「それは bypass ではない」と気付いて cacheTtl: 0 に直しました。ただ、この時点でもまだ「TTL を 0 にすること」と「cache を使わないこと」を混同していました。
最後に公式ドキュメントを読み直して、cacheEverything: false は拡張をやめるだけ、cacheTtl: 0 は TTL 制御、bypass の意図を最も素直に表すのは cache: 'no-store' だと整理できました。
この一連の流れから得た教訓は次のとおり。
-
設定値の名前から動作を推測しない:
cacheEverything: falseは「キャッシュしない」ではなく「キャッシュ拡張をしない」。 -
TTL 制御と bypass は別の話として考える:
cacheTtl: 0はcache: 'no-store'と同じではありません。 -
cache の挙動は origin 側でも確認する:
cf-cache-statusの値だけで bypass を断定しないほうが安全です。
まとめ
-
cacheEverything: falseは bypass ではなく、cacheEverything: trueによるキャッシュ拡張をやめるだけです -
cacheTtl: 0は TTL override であり、bypass を表す標準 API ではありません - bypass の意図を最も素直に表すのは
cache: 'no-store'です - compatibility flag (
request_cf_overrides_cache_rules) が無効だと Workers 側の cache 設定はサイレントに無視されます -
cf-cache-statusの値だけで bypass を断定せず、origin に到達していることを別の観測手段で確認した方が安全です
今回の遠回りは、設定名から挙動を推測してしまったことから始まっていました。Cloudflare Workers の cache 設定は似た名前のオプションが並びますが、キャッシュ拡張のフラグ、TTL の制御、bypass の指定はそれぞれ別物です。こういう場面では、設定名の雰囲気よりも、公式ドキュメントの「何をする API か」という説明に素直に従うほうが、結果的に早く進みます。