03-5211-7750 平日|09:30~18:00

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

           

サービス資料や
ホワイトペーパーはこちら

           資料を【無料】ダウンロードFREE

はじめに

CloudflareからCloudflare Workers Cacheという機能が新しくリリースされました。

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の構成

これまでCloudflare Workersは、リクエストを受けてから処理を実行し、オリジンやバックエンドへつなぐ役割で使われることが多く、構成的に「Workers -> キャッシュ」となっているため、キャッシュが存在してもWorkersが毎回実行される仕組みになってました。

構成図

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

事前準備

検証用のWEBアプリケーションをオリジンサーバに用意します。
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

試しにCloudflareの管理画面からキャッシュパージしてみたのですが、パージされませんでした。
今までのキャッシュとは別の動きをしていることがわかります。

キャッシュをパージするには以下の方法があります。
  • ●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設定

キャッシュさせたくない場合は、該当のパスのレスポンスヘッダに `Cache-Control: private` を付与すればキャッシュさせない設定になります。

`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ヘッダ

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

Workers Cacheでは、レスポンスの圧縮形式をCloudflareが自動的に切り替えるのではなく、Workerが返した Content-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
...

キャッシュキー

Workers Cacheのキャッシュキーについては主に以下がCache Keyに含まれます。
  • ●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

キャッシュ状況の確認

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

▶Cachingの概要ページ画面
Cachingの概要ページ画面
▶Workersのメトリクス画面
Workersのメトリクス画面

最後に

簡単ではありますが、Workers Cacheの機能を確認してみました。
検証している感覚としては、Workersの変更をデプロイしてから切り替わるまで、今までのWorkersより少し時間がかかるように感じました。
また、これまでのCloudflareのキャッシュとは異なり、Workers CacheではWorkersの手前にキャッシュが存在するため、従来のキャッシュとは少し異なる考え方が必要になります。最初はキャッシュの動作や制御方法に戸惑う場面もあるかもしれません。
一方で、使い方によってはWorkersの実行回数を減らすことができ、レスポンスの高速化も期待できます。

ただし、Cloudflareの画面からキャッシュの設定を細かく制御するのではなく、Workers側でキャッシュの挙動を考慮して実装する必要があります。そのため、実際に利用する場合にはこれまで以上にキャッシュの仕組みを理解したうえで設計することが重要だと感じました。
特に、no-cache やキャッシュキーなど、キャッシュに関する基本的な挙動をあらためて確認しておく必要があります。

自由度が高い分、設計や検証で考慮すべきポイントも多い機能という印象です。本番環境で利用する場合には、意図しないキャッシュやデータの混在が発生しないよう、これまで以上にキャッシュの動作を確認しておくことが重要だと思います。

そしてCloudflareなら、Workers Cache以外の機能も組み合わせることで、
  • ●パフォーマンスの向上
  • ●サイトの信頼性の向上
  • ●運用コストの最適化

を、一つのプラットフォーム上で進められます。Cloudflareの導入や運用を検討する際は、アプリケーションの更新頻度と利用者ごとのデータ分離を踏まえ、自社に合ったキャッシュ設計から検討してみてください。

Cloudflareの導入・運用について ご相談いただけます。 導入に関するご相談だけでなく、運用についてもご相談ください。

杉木 俊文

技術本部
プラットフォーム部
Contact usお問い合わせ

サービスにご興味をお持ちの方は
お気軽にお問い合わせください。

Webからお問い合わせ

お問い合わせ

お電話からお問い合わせ

03-5211-7750

平日09:30 〜 18:00

Download資料ダウンロード

製品紹介やお役立ち資料を無料でご活用いただけます。

Magazineメルマガ登録

最新の製品情報などタイムリーな情報を配信しています。