【Cloudflare Workers】Cache Keyをマスターして高度なキャッシュ制御を実現する

はじめに
Cloudflare Blog: https://blog.cloudflare.com/workers-cache/
Cloudflare Doc: https://developers.cloudflare.com/workers/cache/
Workers Cacheは、Workersの手前でHTTPレスポンスをキャッシュする新しい仕組みです。キャッシュヒット時はWorkersそのものが実行されないため、Workers実行回数を増やさずに高速化できます。
ただし、Workers Cacheは、Workers専用のキャッシュです。今までのゾーンレベルのキャッシュ制御とダッシュボードは別系統のキャッシュ上で動作します。
今までのキャッシュの方法とは違う部分が多々あり覚えることが多いので、混同しないように注意が必要です。Cloudflareのドキュメントも確認してみて欲しいのですが、内容にボリュームがあります。
今回の内容でも全部は網羅しきれてないのですが、まずは動かしてみるという試みです。
Workers Cacheの構成

Workers Cacheは、この構成を「キャッシュ → Workers」に反転させます。キャッシュがあればCloudflareがレスポンスを直接返し、Workerの実行とCPU時間課金を回避します。キャッシュミス時のみWorkersが実行され、キャッシュ可能なレスポンスが次回以降のために保存されます。

Workers Cacheの特徴
| 観点 | 従来のWorkers | Workers Cache有効時 |
|---|---|---|
| キャッシュの位置 | Workerの後方にある既存のゾーンキャッシュを設計・設定する | Workerの手前でリクエストを判定する |
| キャッシュヒット | 毎回Workersが実行される | Workersは実行されず、CPU時間は課金されない |
| 制御の中心 | ゾーン設定・ルールとWorker実装を組み合わせる | wrangler設定とHTTPレスポンスヘッダーで制御する |
重要なのは、Workers Cacheがゾーン単位のキャッシュ設定ではなく、「Workerに属するキャッシュ」として動く点です。カスタムドメイン、workers.dev、プレビューURL、サービスバインディングといった呼び出し経路でも、Workerを中心に一貫したキャッシュを扱えます。
今までのキャッシュとWorkers Cacheの違い
| 項目 | 内容 |
|---|---|
| 有効化/TTL | wranglerの設定で有効化 レスポンスのCache-ControlヘッダーでTTLを指定 |
| 階層化 | 地域ごとの下位層とネットワーク全体で共有する上位層からなる、 2層のTiered Cacheを標準で利用できる |
| 分析 | Workersのメトリクス画面でキャッシュステータス等を確認 |
| Cacheルール | Cloudflareの画面からキャッシュルールの指定ができない |
| パージ | Cloudflareの画面からキャッシュパージできない。 Workersのコード内でパージの記述を書く必要がある |
| キャッシュキー | Varyヘッダを使用して分ける |
| キャッシュの仕様 | ホスト名でキャッシュが別にならない |
| Workers更新 | 更新する度にキャッシュが空になる ※クロスバージョンキャッシュを入れることで回避可能 |
その他の覚えておきたい情報
デフォルトでキャッシュしないもの
Cloudflare Doc: https://developers.cloudflare.com/workers/cache/configuration/#automatic-bypass-conditions
Cache-Controlがない場合のキャッシュ時間
Cloudflare Doc: https://developers.cloudflare.com/workers/cache/configuration/#cache-control-semantics
リクエストの集約
Cloudflare Doc: https://developers.cloudflare.com/workers/cache/#request-collapsing
事前準備
CloudflareからオリジンサーバにリバースプロキシでWEBサイトが閲覧できるようにしておきます
※WEBサイトのコンテンツはご自由にご用意ください。

動作検証
Workers Cacheが有効なWorkersの作成
まずworkersを作成します。
$ npm create cloudflare@latest -- workers-cache-01
作成後、型定義をインストールします。
$ cd workers-cache-01
$ npx wrangler types
`wrangler.jsonc`を編集します
※FQDNとZONE_NAMEは環境に合わせて下さい。
Workers Cacheを有効化の設定と、Workersを更新してもキャッシュを保持するようにクロスバージョンキャッシュを有効にします。
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "workers-cache-01",
"main": "src/index.ts",
"compatibility_date": "2026-07-13",
"upload_source_maps": true,
"compatibility_flags": ["nodejs_compat"],
"cache": {
"enabled": true,
"cross_version_cache": true,
},
"route": {
"pattern": "{ FQDN }/*",
"zone_name": "{ ZONE_NAME }",
}
}`src/index.ts`を編集します
export default {
async fetch(request: Request): Promise<Response> {
const originResponse = await fetch(request);
const headers = new Headers(originResponse.headers);
headers.set(
"Cache-Control",
"public, max-age=60, stale-while-revalidate=120",
);
return new Response(originResponse.body, {
status: originResponse.status,
statusText: originResponse.statusText,
headers,
});
},
} satisfies ExportedHandler;wranglerでCloudflareにログインしてコードをデプロイします。
$ npx wrangler login
$ npx wrangler whoami
$ npx wrangler deploy
ブラウザからWorkersを適用したFQDNにアクセスしてみます。
※以下の画面はこちらで作成したサンプルの画面になります。

index.htmlのヘッダーを確認すると、レスポンスヘッダーにWorkersで指定したCache-Controlが付与されます。
何度かリクエストしてみるとHTMLなのにキャッシュHITしていることが確認できます。
※Cloudflareのデフォルトの設定だとhtmlはキャッシュ対象外

WorkersのログをtailしてみるとWorkersのリクエストログが出力されず、Workers自体が実行されてないことが確認できます。
$ npx wrangler tail
Cache PURGE
今までのキャッシュとは別の動きをしていることがわかります。
キャッシュをパージするには以下の方法があります。
- ●tags
- ●pathPrefixes
- ●purgeEverything
Cloudflare Doc: https://developers.cloudflare.com/workers/cache/purge/
今回は動作検証ですのでpurgeEverythingをシンプルに実装してみます。
POSTで `/api/v1/purge/all` にリクエストしたらパージされるようにします。
本番環境では必ず該当のパスに認証を入れて他の人は実行できないようにしたり、該当のメソッド以外は拒否するような設定を入れて下さい。
`src/index.ts`を編集します。
function jsonResponse(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
},
});
}
export default {
async fetch(request: Request, _env: Env,ctx: ExecutionContext,): Promise<Response> {
const url = new URL(request.url);
// 全キャッシュPURGE, POST /api/v1/purge/all
if (
request.method === "POST" && url.pathname === "/api/v1/purge/all"
) {
const result = await ctx.cache.purge({
purgeEverything: true,
});
if (!result.success) {
console.error("Cache purge failed", result.errors);
return jsonResponse(
{
success: false,
errors: result.errors,
},
500,
);
}
return jsonResponse({
success: true,
message: "All cache entries were purged.",
});
}
const originResponse = await fetch(request);
const headers = new Headers(originResponse.headers);
headers.set(
"Cache-Control",
"public, max-age=60, stale-while-revalidate=120",
);
return new Response(originResponse.body, {
status: originResponse.status,
statusText: originResponse.statusText,
headers,
});
},
} satisfies ExportedHandler<Env>;コードをデプロイします。
$ npx wrangler deploy
以下のようにキャッシュが存在していることを確認した後に、キャッシュをパージして実際にパージできることを確認できました。
$ curl -v https://[ FQDN ]/ -o /dev/null
...
< cf-cache-status: HIT
...
$ curl -X POST https://[ FQDN ]/api/v1/purge/all
{"success":true,"message":"All cache entries were purged."}
$ curl -v https://[ FQDN ]/ -o /dev/null
...
< cf-cache-status: MISS
...
No Cache設定
`src/index.ts`を編集します。
以下はリクエストパスが/sample.jpgの場合はキャッシュさせないようにする例です。
if (url.pathname === "/sample.jpg") {
headers.set(
"Cache-Control",
"private",
);
} else {
headers.set(
"Cache-Control",
"public, max-age=60, stale-while-revalidate=120",
);
}コードをデプロイします。
$ npx wrangler deploy
以下のURLにアクセスしてレスポンスヘッダーを確認すると、Cf-Cache-StatusがBYPASSとなってキャッシュされない設定になっていることが確認できました。
URL: https://[ FQDN ]/sample.jpg

Varyヘッダ
Cloudflare Doc: https://developers.cloudflare.com/workers/cache/configuration/#vary
`src/index.ts`を編集します。
Vary: Acceptをレスポンスヘッダにセットすることで、Acceptヘッダの値にてキャッシュを分ける例です。
headers.set(
"Cache-Control",
"public, max-age=60, stale-while-revalidate=120",
);
headers.set("Vary", "Accept");
上記でWorkersをデプロイしてキャッシュパージしておきます。
$ npx wrangler deploy
$ curl -X POST https://[ FQDN ]/api/v1/purge/all
以下コマンドでアクセスすると、別のキャッシュとして保存がされます。
(最初のアクセスはともにcf-cache-statusがMISSになる。2回目以降HITになります。)
$ curl -vk -H "Accept: image/png" https://workers-cache.xxxx.xxx/logo.png
...
< cf-cache-status: MISS
...
$ curl -vk -H "Accept: image/png, image/*" https://workers-cache.xxxx.xxx/logo.png
...
< cf-cache-status: MISS
...
ただ、上記ですとAcceptの種類や並びの順番の分だけキャッシュが別れます。
※別々のキャッシュとして保存されます。
実際に運用する場合にはAccept部分の正規化などしないと不必要にキャッシュが別れることになりますのでご注意ください。
※後述するGateway Workersなどを使用して正規化することをお勧めします。
Vary時のキャッシュのパージについてですが、Varyによって分けたキャッシュは単一のパージIDを共有します。
そのため、プレフィックス、タグパージともに、まとめてコンテンツのパージが可能です。
ただ、タグの場合は、Varyで分けたキャッシュコンテンツごとに別々のタグをつけている場合はその限りではありませんのでご注意ください。
また、Cloudflare polishを使用する場合にはVaryは互換性がないため、使用する場合には注意が必要です。
Cloudflare Doc: https://developers.cloudflare.com/workers/cache/configuration/#vary
Accept-Encoding
したがって、クライアントごとに gzip、br、非圧縮等を切り替えたい場合は、以下の2パターンとなります。
- ●Worker内で正規エンコーディング形式を1つ選択
- ●VaryでAccept-Encodingを分ける
Cloudflare Doc: https://developers.cloudflare.com/workers/cache/configuration/#accept-encoding-and-content-encoding
Varyの場合は、Workersのエントリーポイントを多段(以下図のGateway Workers)にして、前段の処理でAccept-Encoding を正規化し、キャッシュを制御するようドキュメントに記載があります。

Gateway Workers側でAccept-Encodingの値を確認し正規化して、後段のWorkersに処理を渡します。
Gateway Workers側ではWorkers Cacheを無効にして、後段のWorkers側でWorkers Cacheを有効にする形で実装します。

今回は、br → gzip → deflate の優先順で判定し、1種類に正規化してみます。
対応する方式がない場合は identity とします。
例えば Accept-Encoding: gzip, deflate, br の場合は br に正規化されます。
`src/index.ts`を編集します。
import { WorkerEntrypoint } from "cloudflare:workers";
type EncodingVariant = "br" | "gzip" | "deflate" | "identity";
function jsonResponse(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
},
});
}
function selectEncoding(request: Request): EncodingVariant {
const acceptEncoding =
request.cf?.clientAcceptEncoding?.toLowerCase() ?? "";
if (acceptEncoding.includes("br")) {
return "br";
}
if (acceptEncoding.includes("gzip")) {
return "gzip";
}
if (acceptEncoding.includes("deflate")) {
return "deflate";
}
return "identity";
}
export class Backend extends WorkerEntrypoint<Env> {
async purgeAll() {
return this.ctx.cache.purge({
purgeEverything: true,
});
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const originResponse = await fetch(request);
const headers = new Headers(originResponse.headers);
if (url.pathname === "/sample.jpg") {
headers.set("Cache-Control", "private");
} else {
headers.set(
"Cache-Control",
"public, max-age=60, stale-while-revalidate=120",
);
headers.set("Vary", "Accept, Accept-Encoding");
}
return new Response(originResponse.body, {
status: originResponse.status,
statusText: originResponse.statusText,
headers,
});
}
}
/**
* 外部からのリクエストを受け付けるGateway entrypoint。
*
* wrangler.jsoncでキャッシュを無効にするため、
* リクエストごとに必ず実行される。
*/
export default {
async fetch(
request: Request,
_env: Env,
ctx: ExecutionContext,
): Promise<Response> {
const url = new URL(request.url);
if (
request.method === "POST" &&
url.pathname === "/api/v1/purge/all"
) {
const result = await ctx.exports.Backend.purgeAll();
if (!result.success) {
console.error("Backend cache purge failed", result.errors);
return jsonResponse(
{
success: false,
errors: result.errors,
},
500,
);
}
return jsonResponse({
success: true,
message: "All backend cache entries were purged.",
});
}
/**
* クライアント本来のAccept-Encodingを、
* br / gzip / deflate / identityのいずれかに正規化する。
*/
const encoding = selectEncoding(request);
const headers = new Headers(request.headers);
headers.set("Accept-Encoding", encoding);
const normalizedRequest = new Request(request, {
headers,
});
/**
* 正規化後のAccept-Encodingで、
* キャッシュ有効なBackend entrypointを呼び出す。
*/
return ctx.exports.Backend.fetch(normalizedRequest);
},
} satisfies ExportedHandler<Env>;`wrangler.jsonc`を編集します。
entrypointごとにキャッシュの有効/無効を設定しています。
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "workers-cache-01",
"main": "src/index.ts",
"compatibility_date": "2026-07-13",
"upload_source_maps": true,
"compatibility_flags": ["nodejs_compat"],
"cache": {
"enabled": true,
"cross_version_cache": true,
},
"route": {
"pattern": "{ FQDN }/*",
"zone_name": "{ ZONE_NAME }",
}
"exports": {
// Gatewayは毎回実行するためキャッシュ無効
"default": {
"type": "worker",
"cache": {
"enabled": false
}
},
// Backendの手前ではキャッシュを有効化
"Backend": {
"type": "worker",
"cache": {
"enabled": true
}
}
}
}上記でWorkersをデプロイしてキャッシュパージしておきます。
$ npx wrangler deploy
$ curl -X POST https://[ FQDN ]/api/v1/purge/all
以下コマンドでアクセスすると、別のキャッシュとして保存がされます。
(最初のアクセスはともにcf-cache-statusがMISSになる)
$ curl -vk -H "Accept-Encoding: gzip" https://[ FQDN ]/ -o /dev/null
...
< cf-cache-status: MISS
...
$ curl -vk -H "Accept-Encoding: deflate" https://[ FQDN ]/ -o /dev/null
...
< cf-cache-status: MISS
...
キャッシュキー
- ●Worker の entrypoint
- ●URLのPATH
- ●Query String ※パラメータの順番も区別される。/foo?a=1&b=2 と /foo?b=2&a=1 は別
- ●Workers Version
- ●Service Binding / RPC の場合は ctx.props
一方、ホスト名 は キャッシュキー に含まれません。
そのため同じ Workers ならホスト名が違っても、PATH/Query が同じなら同じキャッシュを共有できます。
- ●api.example.com/foo
- ●api.example.net/foo
また、Workers内でカスタムキャッシュキーを設定することも可能です
Cloudflare Doc: https://developers.cloudflare.com/workers/cache/cache-keys/#custom-cache-keys
カスタムキャッシュキーでできること
| 1 | URLの一部を無視します。トラッキングパラメータ(utm_source、gclid)の削除や、クエリ文字列を完全に削除して、レスポンスを1つにする。 |
| 2 | URLが違っても同じリソースならまとめます。URLではなく共通の識別子をキーにすることで、複数のURLを1つのキャッシュにまとめることができます。 |
| 3 | キャッシュを分割します。キーに識別値を追加することで、競合する可能性のあるリクエストを別々のエントリとして強制的に作成できます。 |
カスタムキャッシュを使用して、キャッシュをカスタマイズしてみます。
パラメータのutm_sourceを無視してキャッシュを行うように変更してみます。
- ●URL1: https://[ FQDN ]/?utm_source=google
- ●URL2: https://[ FQDN ]/?utm_source=facebook
上記は1回目のアクセスはキャッシュMISSとなります。
> cf-cache-status: MISS
Workersのコードを変更します。
const normalizedRequest = new Request(request, {
headers,
});
/**
* 正規化後のAccept-Encodingで、
* キャッシュ有効なBackend entrypointを呼び出す。
*/
return ctx.exports.Backend.fetch(normalizedRequest);↓
const normalizedRequest = new Request(request, {
headers,
});
// カスタムキャッシュキーを作成
const cacheKeyUrl = new URL(request.url);
// キャッシュ内容に影響しないパラメータを除外
cacheKeyUrl.searchParams.delete("utm_source");
/**
* 正規化後のAccept-Encodingで、
* キャッシュ有効なBackend entrypointを呼び出す。
*/
return ctx.exports.Backend.fetch(normalizedRequest, {
cf: {
cacheKey: cacheKeyUrl.pathname + cacheKeyUrl.search,
},
});上記でWorkersをデプロイしてキャッシュパージしておきます。
$ npx wrangler deploy
$ curl -X POST https://[ FQDN ]/api/v1/purge/all
以下にアクセスするとキャッシュがMISSになります。
https://[ FQDN ]/?utm_source=google
> cf-cache-status: MISS
以下にアクセスするとキャッシュがHITになっていることが確認できました。
https://[ FQDN ]/?utm_source=facebook
> cf-cache-status: HIT
キャッシュ状況の確認

既存のCachingの概要ページを確認したところ、値は表示できていたのですがキャッシュステータスの数値が一致しない状況が確認できました。
そもそもWorkers Cacheは既存のゾーンキャッシュとは別扱いとなっており、公式でもWorkersの画面から確認できると記載がありますので、メトリクスの画面から確認する値が正確と考えられます。
▶Cachingの概要ページ画面

▶Workersのメトリクス画面

最後に
検証している感覚としては、Workersの変更をデプロイしてから切り替わるまで、今までのWorkersより少し時間がかかるように感じました。
また、これまでのCloudflareのキャッシュとは異なり、Workers CacheではWorkersの手前にキャッシュが存在するため、従来のキャッシュとは少し異なる考え方が必要になります。最初はキャッシュの動作や制御方法に戸惑う場面もあるかもしれません。
一方で、使い方によってはWorkersの実行回数を減らすことができ、レスポンスの高速化も期待できます。
ただし、Cloudflareの画面からキャッシュの設定を細かく制御するのではなく、Workers側でキャッシュの挙動を考慮して実装する必要があります。そのため、実際に利用する場合にはこれまで以上にキャッシュの仕組みを理解したうえで設計することが重要だと感じました。
特に、no-cache やキャッシュキーなど、キャッシュに関する基本的な挙動をあらためて確認しておく必要があります。
自由度が高い分、設計や検証で考慮すべきポイントも多い機能という印象です。本番環境で利用する場合には、意図しないキャッシュやデータの混在が発生しないよう、これまで以上にキャッシュの動作を確認しておくことが重要だと思います。
そしてCloudflareなら、Workers Cache以外の機能も組み合わせることで、
- ●パフォーマンスの向上
- ●サイトの信頼性の向上
- ●運用コストの最適化
を、一つのプラットフォーム上で進められます。Cloudflareの導入や運用を検討する際は、アプリケーションの更新頻度と利用者ごとのデータ分離を踏まえ、自社に合ったキャッシュ設計から検討してみてください。
サービスにご興味をお持ちの方は
お気軽にお問い合わせください。
Webからお問い合わせ
お問い合わせお電話からお問い合わせ
平日09:30 〜 18:00











