Cloudflare Workers に独自キャッシュ機能を追加
Cloudflare は Workers の前に階層キャッシュを配置する「Workers Cache」機能をリリースし、設定コードの追加と HTTP ヘッダー制御のみで、実行コスト削減とパフォーマンス向上を実現可能にした。
キーポイント
事前キャッシュによるコスト削減と高速化
リクエストがワーカーの実行前に Cloudflare キャッシュにヒットする場合、ワーカーは起動せず CPU 使用料が発生しないため、コストとレイテンシを劇的に削減できる。
コードベースでの直感的な設定
Wrangler の単一設定ブロックで有効化し、既存の Cache-Control ヘッダーやプログラムmatic なパージ機能(タグ指定など)を用いて、従来の HTTP 標準通りのキャッシュ制御が可能になる。
階層型アーキテクチャとエントリポイント制御
単一ワーカー内の複数のエントリポイントごとに個別にキャッシュを構成でき、アプリケーション構造に合わせてキャッシュステージを組み込む柔軟な設計となっている。
アーキテクチャの転換:キャッシュがワーカーの手前に配置される
従来の「ワーカー→キャッシュ→オリジン」から、「キャッシュ→ワーカー」という逆転した構造へ変更され、キャッシュヒット時にはワーカーの実行自体がスキップされる。
サーバーサイドレンダリングにおけるコストと遅延の解消
以前はビルド時の静的生成か、全リクエストでの実行かの二者択一だったが、この新機能により最新コンテンツを維持しつつ、重複するページロードのコストと遅延を回避できるようになる。
ワーカーがオリジンとなった現代の要件への対応
Next.jsやSvelteKitなどのフレームワークがCloudflare上で直接動作しワーカー自体がサーバーとなるケースが増えたため、従来のモデルではキャッシュ対象がなく非効率だった問題を解決する。
静的サイトの速度とサーバーレンダリングの鮮度を両立
ビルド時間を要せず、キャッシュ期限切れ時に背景で再レンダリングを行うことで、ユーザーが待たずに最新コンテンツを提供できます。
重要な引用
If there's a fresh cached response, Cloudflare returns it directly — your Worker doesn't run, and you don't pay CPU time for it.
There is no zone to configure, no rules engine to set up, no separate cache to provision, and no second product to log into.
Workers Cache is available today to every Worker on any plan, enabled in Wrangler.
"Workers Cache flips the architecture. Cloudflare's cache now sits in front of the Worker:"
"On a cache hit, your Worker doesn't run at all. Cloudflare returns the cached response and your CPU billing stays at zero."
You get the speed of a static site without the build time, and the freshness of server rendering without the cost.
影響分析・編集コメントを表示
影響分析
この発表は、Serverless アーキテクチャにおけるコスト効率とパフォーマンスのボトルネックである「すべてのリクエストが計算リソースを消費する」という課題に対する決定的な解決策となる。開発者は複雑なキャッシュ設定やインフラ管理に悩まされることなく、既存の HTTP 知識だけで大規模なキャッシュ戦略を実装でき、Cloudflare エコシステム全体の利用価値が向上する。
編集コメント
従来のサーバーレス環境では避けて通れなかった「キャッシュと計算の分離」の問題を、インフラ設定の複雑化なしに解決した画期的なアップデートです。特に大規模なトラフィックを扱うアプリケーションにおいて、コスト構造そのものを変える可能性を秘めています。
本日、Workers Cache をリリースします。これはワーカーの手前に配置される階層型キャッシュで、Wrangler 設定の単一行と、すでに熟知されている Cache-Control ヘッダーによって構成されます。
Workers Cache が有効化されると、ワーカーに対するすべてのキャッシュ可能なリクエストはまず Cloudflare のキャッシュにヒットします。新鮮なキャッシュレスポンスが存在する場合、Cloudflare はそれを直接返却し、ワーカーは実行されず、CPU 使用時間に対して課金されることもありません。キャッシュミスが発生した場合、ワーカーが実行され、レスポンスがキャッシュ可能であれば、Cloudflare が次のリクエストのために保存します。地球上のどこからのリクエストでも、キャッシュから直接提供できるようになります。

全体が単一の設定ブロックで構成されています:
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-01",
"cache": {
"enabled": true
}
}
その後、HTTP が常に望んでいた通り、レスポンスにヘッダーを設定することでキャッシュ制御を行います:
return new Response(body, {
headers: {
"Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
"Cache-Tag": "products,product:123",
},
});
そして、コンテンツが変更された場合、ワーカーは自身のキャッシュをパージします:
return new Response(body, {
headers: {
"Cache-Control": "public, max-age=0, stale-while-revalidate=86400",
"Cache-Tag": "products,product:123",
},
});
await ctx.cache.purge({ tags: ["product:123"] });
これが API のすべてです。設定すべきゾーンも、セットアップするルールエンジンも、用意すべき別個のキャッシュも、ログインすべき第二の製品もありません。Worker のコード自体が設定インターフェースであり、キャッシュは Worker がどこで実行されてもそれに追随します — カスタムドメイン上でも、workers.dev 上でも、サービスバインド背後でも、プレビュー中であっても、Workers for Platforms テナント内であっても。一つの Worker に一つのキャッシュ、一度設定すればそれで十分です。
これが表面に現れる機能範囲ですが、その下には多くの仕組みが備わっています:ネットワーク全体にわたる階層型キャッシング、stale-while-revalidate の完全なサポートにより古いレスポンスがユーザーをブロックすることのない仕組み、Vary によるコンテンツネゴシエーション、ctx.props を介したマルチテナント対応のキャッシュキー、タグまたはパスプレフィックスによるプログラムによるパージ、そして — 私たちが最も大きな突破口だと考えている部分 — すべての Worker エントリーポイントの前に配置されるキャッシュです。公開エントリーポイントだけでなく、どのエントリーポイントをキャッシュし、どのエントリーポイントをキャッシュしないかをエントリーポイントごとに制御できます。この最後の機能により、キャッシュをアプリの構造に直接組み込むことが可能になります:キャッシュステージを任意の位置にスロットインしたエントリーポイントのチェーンを、その両側のコードによって構成できるのです。これらすべてについて以下で詳しく解説します。
Workers Cache は今日から、すべてのプラン上のすべての Worker で利用可能です。Wrangler で有効化するだけで使用できます。
これは、私たちが Workers にずっと欲しかったキャッシング API です。なぜこれほど時間がかかったのか、これが可能にしたこと、そして今後何が来るのかについて説明します。
サーバーレンダリングされたアプリが前方キャッシュを必要とする理由
2017 年に Workers を導入した際、その訴求点は、Cloudflare のネットワーク上でコードを実行して、オリジンへのリクエストを転送中に変換できることでした。Worker はキャッシュとオリジンの前に位置していました:

これは、私たちがターゲットとしていたユースケースにとって適切なモデルでした。すべてのリクエストにヘッダーを追加したり、URL を書き換えたり、A/B 分割を行ったり、オリジンに到達する前にトラフィックをフィルタリングしたい場合、キャッシュとオリジンの前に Worker を配置することで、何がキャッシュされ何がされないかを完全に制御できました。顧客たちはこれを使って素晴らしいものを構築しました。
しかし、世界は変わりました。Workers はもはやオリジンに取り付けるものではなく、それ自体がオリジンとなりました。Astro、TanStack Start、Next.js、Remix、SvelteKit といったフレームワークはすべて、アプリを Worker として構築する Cloudflare アダプターを搭載して提供されています。その背後にオリジンは存在しません。Worker がサーバーそのものです。
ワーカーがオリジンである場合、従来のアーキテクチャにはキャッシュするものがありません。すべてのリクエストがコードを実行し、直前に返したレスポンスとバイト単位で完全に一致する場合でも同様です。Workers ランタイムは十分に高速であり、汗をかくことなく毎秒数千万件のリクエストを処理するのが常ですが、「すべてのリクエストをレンダリングするのに十分速い」という状態でも、ページ読み込みごとにレイテンシが発生し、各呼び出しで CPU 時間を消費します。そしてサーバーサイドレンダリングされたアプリケーションでは、定義上、すべてのページ読み込みがレンダリングとなります。
Workers Cache はアーキテクチャを逆転させます。Cloudflare のキャッシュがワーカーの手前に配置されるようになります:
image
キャッシュヒット時にはワーカーは全く実行されません。Cloudflare がキャッシュされたレスポンスを返し、CPU 課金はゼロのままになります。キャッシュミス時にはワーカーが一度実行されてキャッシュにデータを格納し、その後のリクエスト(どこからでも)はコードを実行せずにキャッシュから提供されます。
これが Workers におけるサーバーサイドレンダリングで欠けていた機能です。従来は、2 つの満足できない選択肢の間で選ぶ必要がありました:
ビルド時にすべてをプリレンダリングする(「静的サイト生成」)。ページ読み込みは高速ですが、変更があるたびに完全な再構築と再デプロイが必要です。数千ページのドキュメントサイトであれば 5〜10 分かかりますが、大規模な e コマースサイトではさらに悪化し、何かを触るたびにビルドが実行されます。
リクエストごとにすべてのページをレンダリングします。最新の内容を保証できますが、ページ読み込みのたびにレンダリングコストが発生し、すべての訪問者がレイテンシを負担することになります。
Workers Cache は第 3 の選択肢を提供します:オンデマンドでサーバーサイドレンダリングを行い、レンダリングされたレスポンスをキャッシュし、あなたが設定した有効期限(TTL)に基づいて更新します。新しいページへの最初のリクエストでは依然としてレンダリングが行われますが、キャッシュの有効期限内のその後のすべてのリクエストは、ページが静的であるかのように提供されます。キャッシュが期限切れになると、次のリクエストで再レンダリングがトリガーされますが、stale-while-revalidate( stale-while-revalidate)を使用すれば、その待ち時間さえも不要になります。
ビルド時間を要しない静的サイトの速度と、コストを伴わないサーバーサイドレンダリングの鮮度を手に入れることができます。Incremental Static Regeneration(インクリメンタル・スタティック・ジェネレーション)のようなフレームワーク固有の仕組みは不要です。単に HTTP キャッシュが本来の設計通りに動作し、オリジンとして設計されたコードの前で機能するだけです。
stale-while-revalidate が瞬時に感じるようにさせる部分です
stale-while-revalidate ディレクティブは、Cloudflare に対して、キャッシュされたレスポンスが期限切れになった際、背景でレスポンスを更新しながら、すぐに古くなったコピーを提供することを許可します。Cloudflare は今年前半に stale-while-revalidate の完全なサポートを実装しましたが、これが「Worker をキャッシュする」を「Worker のサイトが静的であるかのように感じる」に変えるディレクティブです。
キャッシュエントリが期限切れになった後の最初の要求では、ワーカがページを最初からレンダリングするまで待たなければなりません。ユーザーはその遅延を見ます。これがある場合、期限切れ後の最初の要求はすぐに古いページを受け取り(Cf-Cache-Status: UPDATING ヘッダー付き)、ワーカはバックグラウンドでキャッシュの再充填を実行します。リフレッシュトリガーを行ったユーザーを含むすべてのユーザーが、キャッシュ速度でのレスポンスを得られます。

実際には、次のようになります。
export default {
async fetch(request) {
const html = await renderPage(request);
return new Response(html, {
headers: {
"Content-Type": "text/html; charset=utf-8",
// Treat as fresh for 5 minutes; serve stale for up to an hour
// while a background refresh runs.
"Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
},
});
},
};
これが理解できるためのメンタルモデルは以下の通りです。
フレッシュウィンドウ(max-age): Cloudflare がキャッシュされたレスポンスを提供します。あなたのワーカは実行されません。
ステイルウィンドウ(stale-while-revalidate): Cloudflare がキャッシュされたレスポンスを提供します。あなたのワーカはバックグラウンドで実行されて更新されます。ユーザーは一切待ちません。
両方のウィンドウの外側: Cloudflare があなたのワーカを実行して新しいレスポンスを生成し、ユーザーはそのレンダリングを待ちます。
あなたはウィンドウを選択します。数分ごとに更新される製品カタログの場合、max-age=300, stale-while-revalidate=3600 と設定すれば、訪問者は基本的に待たされず、ワーカーもコンテンツを新鮮に保つために十分な頻度で実行されます。一方、ほとんど変更されないブログアーカイブの場合、max-age=86400, stale-while-revalidate=2592000 と設定すれば、ページごとにワーカーは1日に1回だけ実行されます。
新規作成されたページに対する最初のリクエストのみが、完全なレンダリングコストを負担します。その後、そのページは訪問者にとっては静的出力のように動作しますが、ページの生成方法を管理するのは依然としてワーカーです。
一つの URL、多くの表現:Vary の働き
実際のアプリケーションでは、すべてのクライアントに同じバイト列を返すことはめったにありません。同じ製品ページでも、ブラウザには HTML として、API クライアントには JSON として返されます。同じ画像でも、WebP をサポートするクライアントには WebP 形式で、そうでないクライアントには JPEG 形式で返されます。また、ユーザーに応じてホームページは英語、フランス語、または日本語で返されることもあります。
キャッシュなしでこれを行うのは簡単です — ワーカーがリクエストヘッダーを読み取り、適切なものを返すだけです。しかし、キャッシュを使ってこれを実現しようとすると、通常は問題が発生します。ほとんどのキャッシュシステムでは、2 つの悪い選択肢しかありません:複数の表現を持つ URL では何もキャッシュしないか、1 つの表現だけをキャッシュして全員に提供するかです。
Workers Cache は、この問題を解決するための正しい方法である標準的な HTTP Vary ヘッダーをサポートしています。あなたの Worker が Vary: Accept-Encoding(または Accept, Accept-Language、その他のリクエストヘッダー)を含むレスポンスを返す場合、Cloudflare はそれらのヘッダーの各一意な組み合わせごとに別々のキャッシュされたバリアントを保存し、送信されてきたリクエストの値と保存された値が一致するバリアントのみを返します。
export default {
async fetch(request) {
const accept = request.headers.get("Accept") ?? "";
const wantsWebp = accept.includes("image/webp");
const body = wantsWebp ? await fetchWebpImage() : await fetchJpegImage();
return new Response(body, {
headers: {
"Content-Type": wantsWebp ? "image/webp" : "image/jpeg",
"Cache-Control": "public, max-age=3600",
// Cache a separate variant per distinct Accept header value.
Vary: "Accept",
},
});
},
};
1 つの URL に 2 つのキャッシュされたバリアントが存在します。Accept: image/webp,*/* を送信するブラウザは WebP を取得し、Accept: image/jpeg を送信するブラウザは JPEG を取得します。どちらもキャッシュから提供されます。あなたの Worker は、それぞれに対して最初のリクエスト時に両方のバリアントを書き込み、その後はどちらのリクエストに対しても 0 回実行されます。
これはコンテンツネゴシエーションのための古くからある HTTP 標準であり、Workers Cache は RFC 9110 および RFC 9111 で記述されている通りに実装されています。Vary に指定できるヘッダーのホワイトリストはありません。必要なものを何でもリストアップすれば、Cloudflare はその文字通りの値に基づいてバリアントをキー付けします。ドキュメントではエッジケースについても解説しており、ゲートウェイ Worker 内でヘッダーを正規化してバリアントのファンアウトを制御する方法や、パージが URL のすべてのバリアントをまとめて無効化する理由、そしてキャッシュ自体を完全に無効にする唯一の場合(Vary: *)について取り上げています。
これはあなたの Zone のキャッシュではなく、Worker 自身のキャッシュです
これらすべてによって可能になることに触れる前に、名指しする価値のある概念的な転換点があります。
Cloudflare には昔からキャッシュが存在します。それは Zone レベルで構成されています:Cache Rules(キャッシュルール)、Page Rules(ページルール)、キャッシュ対象ファイル拡張子のリスト、Cache Reserve(キャッシュリザーブ)、Tiered Cache トポロジー、カスタムキャッシュキーなどです。これらすべては Zone ごとに設定され、歴史的に Worker はその Zone の構成に適合するか、それを回避する形で動作する必要がありました。
Workers Cache は異なります。それはあなたの Worker のキャッシュであり、Worker に属し、Zone に属するものではありません。これには多くの結果が伴い、実際には重要な意味を持ちます:
管理すべき Zone 設定は存在しません。Cache Rules(キャッシュルール)、キャッシュレベルの設定、ファイル拡張子のリスト、Page Rules(ページルール)など、これらすべては Workers Cache には適用されません。Worker の Cache-Control ヘッダーが構成そのものとなります。
キャッシュはホスト名ではなくワーカーに追従します。api.example.com、api.example.net にバインドされ、サービスバインディングを介して呼び出されるワーカーは、これら 3 つすべてで共通のキャッシュを共有します。/users/42 へのリクエストは、どの経路から来たかに関わらず、同じキャッシュされたエントリにヒットします。
キャッシュは workers.dev でも動作します。プレビュー URL でも動作します(各プレビューには独自のキャッシュが割り当てられるため、変更のテストが本番環境を汚染することはありません)。Workers for Platforms でも動作します(各ユーザーワーカーには独自のキャッシュがあり、ディスパッチャーや他のテナントから隔離されています)。これらすべては以前はキャッシュにおいて準二等公民扱いされていましたが、もはやそうではありません。
プurge はワーカーのエントリーポイントにスコープされます。ctx.cache.purge({ purgeEverything: true }) を呼び出すと、そのワーカーのエントリーポイントのキャッシュのみがクリアされ、ゾーンの他のコンテンツをすべて消し飛ばすリスクはありません。あるワーカーのデプロイが別のワーカーのデータを無効化するリスクもありません。
キャッシングに関する設定はコード内で行います:どのパスに長い TTL を適用するか(パスで分岐して異なる max-age を設定する)、どのリクエストをキャッシュからバイパスさせるか(Cache-Control: private を返す)、キャッシュキーがどのように構成されるか(ctx.props に何を含めるかを制御し、ディスパッチ前にゲートウェイワーカーで URL を正規化する)。あなたがすでに記述したワーカーこそが、設定のインターフェースです。
詳細については、Workers Cache: your Worker's cache のドキュメントをご覧ください。
2 つのティア、すべてのワーカー、設定不要
Workers Cache はデフォルトで地域別にティア化されています。2 つのレイヤーがあります:
ユーザーに最も近い Cloudflare データセンター内の下位階層キャッシュです。あなたの Worker へのトラフィックを受信するすべてのデータセンターには、それぞれ独自の上位階層キャッシュが用意されています。
ネットワーク全体でリクエストを集約する上位階層キャッシュです。これは数が少なく、下位階層キャッシュはミス(未ヒット)時に常にこの上位階層に問い合わせを行います。
リクエストはまず下位階層に到達します。ヒットした場合はレスポンスが即座に提供され、処理は完了となります。ミスの場合、下位階層は上位階層に問い合わせます。そこでヒットすればレスポンスが返され、その経路で下位階層にも保存されます。両方の階層でミスした場合のみ、実際にあなたの Worker が実行され、その実行結果が両方の階層に保存されます。

これが重要である理由は、世界中のどこかの最初のリクエストが上位階層を Populate(充満)させるからです。その後のすべてのリクエストは、どのデータセンターからであっても、あなたの Worker を実行することなく上位階層から提供できます。たとえそのデータセンターの下位階層キャッシュでそのリクエストを一度も見たことがなくても同様です。単一のフラットなキャッシュレイヤーの場合と比較してキャッシュヒット率は劇的に向上し、これはまさにあなたの Worker がオリジン(Origin)である場合に最も望ましい結果です。
これは現在、ゾーンに対して Tiered Cache(階層型キャッシュ)として機能しているのと同じトポロジーですが、ユーザーが設定を行う必要はありません。"Worker のために階層型キャッシュをオンにする" というダイアログも存在しません。キャッシング機能が有効化されたすべての Worker には、自動的に無料でこの階層型キャッシュが適用されます。
もしあなたの Worker が Smart Placement を使用している場合、キャッシュはそれとシームレスに連携します:ティアがまず参照され、両方がミスした場合のみ、Smart Placement によって実行がオリジンに近い場所でルーティングされます。これらのレイヤーがどのように相互作用するか、そして平滑化を計画しているいくつかの粗い部分についても、ドキュメントで詳しく説明しています。
ユーザーとデータの近くでアプリを実行する
ウェブパフォーマンスには、誰も完全に解決できていない recurring な緊張関係があります:コードをユーザーの近くに実行したい(なぜならユーザーとサーバー間の往復がクリティカルパスにあるため)、そしてコードをデータの近くに実行したい(なぜならすべてのデータベースクエリもまた往復だからです)。どちらか一方を選べば、もう一方が遅くなります。
私たちは長年にわたり、この両方を追求してきました。当社のネットワークは、世界のインターネットユーザーの約 95% を約 50ms の距離に収めています。Smart Placement と Placement Hints を使用すれば、クラウドリージョンについて考えることなく、コードをデータの近くに保つことができます。しかしこれまで、これら 2 つの要素は完全に連携していませんでした。「ユーザーに近い」か「データに近い」かのどちらかしか選択できず、アプリの両方の半分を同時に正しい場所に配置したい場合は、Cloudflare の専門家である必要がありました。私たちはもっと良い方法があると考えていました。
Workers Cache は、そのギャップを埋める役割を果たします。キャッシュはゾーンではなく Worker に属しており、Worker 間のサービスバインディングや ctx.exports 呼び出しもキャッシュを経由するため、各 Worker が適切な場所で実行されるようにし、それらの間にキャッシュを継ぎ目として配置することで、Worker の連鎖としてアプリケーションを構築できます。
アーキテクチャは以下のようになります:
image
Worker A はユーザーの近くに実行されます。すべてのリクエストにおける低コストでレイテンシに敏感な部分を処理します:認証、レート制限、ルーティング、ヘッダーの正規化、データに依存しない HTML ページの外部「シェル」のレンダリングなどです。
Worker B は、Smart Placement または明示的な配置ヒントにより、データの近くに実行されます。ここでは重労働を担当します:データを取得するページのサーバーサイドレンダリング、製品カタログの読み込み、検索結果の生成、API の集約、高コストな変換処理などです。
Workers Cache は Worker B の手前に位置しています。Worker A がサービスバインディングを介して Worker B を呼び出す際、Cloudflare はまず Worker B のキャッシュを確認します。キャッシュヒットの場合、Worker A はレスポンスを受け取り、Worker B は全く実行されません—データセンター間のホップもデータベースクエリもレンダリング作業も発生しません。
キャッシュヒットパスは次のようになります:ユーザー → ユーザーに近い Worker A → Worker B でのキャッシュヒット → レスポンス。データ転送にかかるコストは、キャッシュミスが発生した場合のみ支払われます。ホットページは「ユーザーの前にコードが配置された」速度で動作し、コールドページも実行時にはデータの近くに配置されることで恩恵を受け続けます。
これを達成するために特別なアーキテクチャを設計する必要はありません。アプリを 2 つの Worker として記述し、1 つをもう 1 つにサービスバインディングで指し示し、Worker B の wrangler.jsonc ファイルでキャッシングを有効にするだけで完了です。

デフォルトでマルチテナントに対応し、ctx.props を活用
ユーザー固有のデータを返す Worker をキャッシュする場合(ログインしたユーザーごとに異なるコンテンツを提供する API など)、あるユーザーが別のユーザーのキャッシュレスポンスを決して見られないようにする必要があります。標準的な解決策は「認証されたリクエストはキャッシュしない」ことであり、Cloudflare の Authorization ヘッダーに対する自動バイパス機能もまさにこれを実現しています。しかし、「何もキャッシュしない」というアプローチでは、パフォーマンス向上の恩恵すべてを放棄することになります。
Workers Cache は、呼び出し元の ctx.props をキャッシュキーの一部に含めることでこの課題を解決します。ある Worker がサービスバインディングを介して別の Worker を呼び出す際、ユーザー ID やテナント ID、あるいは他の識別子を ctx.props として渡すことで、異なる props を持つ呼び出し元はそれぞれ独立したキャッシュエントリを取得します。あるユーザーのレスポンスが、別のユーザーのキャッシュに漏れることは決してありません。
import { WorkerEntrypoint } from "cloudflare:workers";
interface Props { userId: string; }
export default class Backend extends WorkerEntrypoint {
async fetch(request: Request): Promise<void> {
// ctx.props.userId はキャッシュキーの一部です。ユーザー A とユーザー B が同じ URL をリクエストしても、別々のキャッシュエントリが作成されます。
const { userId } = this.ctx.props;
const data = await loadUserData(userId);
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=300",
},
});
}
}
一般的なパターンとしては、ゲートウェイ Worker でリクエストの認証を行い、Authorization ヘッダーを削除し、認証済みユーザーの ID を ctx.props に設定してから、キャッシュされたバックエンド Worker を呼び出します。ゲートウェイはすべてのリクエストで実行されます(認証するためには必須ですが)、高負荷なバックエンドが実行されるのは、そのユーザーに対してまだキャッシュエントリが存在しない場合のみです。認証済みの API は「キャッシュ不可能」から「完全な安全性を維持したユーザーごとのキャッシュ可能」へと進化し、キャッシュキーが自動的に分離処理を行います。この詳細については、M のドキュメントで解説されています。
原文を表示
Today we are launching Workers Cache: a tiered cache that sits in front of your Worker, configured by a single line of Wrangler config and the same Cache-Control headers you already know.
When Workers Cache is enabled, every cacheable request to your Worker hits Cloudflare's cache first. If there's a fresh cached response, Cloudflare returns it directly — your Worker doesn't run, and you don't pay CPU time for it. On a miss, your Worker runs, and if your response is cacheable, Cloudflare stores it for the next request. The next request from anywhere on Earth can be served straight from cache.
image
The whole thing is one config block:
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-01",
"cache": {
"enabled": true
}
}
After that, you control caching the way HTTP has always wanted you to — by setting headers on your responses:
return new Response(body, {
headers: {
"Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
"Cache-Tag": "products,product:123",
},
});
And when content changes, your Worker purges its own cache:
await ctx.cache.purge({ tags: ["product:123"] });
That's the whole API. There is no zone to configure, no rules engine to set up, no separate cache to provision, and no second product to log into. The Worker's code is the configuration surface, and the cache follows the Worker wherever it runs — on a custom domain, on workers.dev, behind a service binding, in a preview, in a Workers for Platforms tenant. One Worker, one cache, configured once.
That's the surface area. There’s a lot underneath: tiered caching across our entire network, full support for stale-while-revalidate so stale responses never block a user, content negotiation via Vary, multi-tenant-safe cache keys via ctx.props, programmatic purges by tag or path prefix, and — the part we think is the biggest unlock — a cache that sits in front of every Worker entrypoint, not just the public one, with per-entrypoint control over which ones cache and which don't. That last piece means you can compose caching directly into the structure of your app: a chain of entrypoints with cache stages slotted in wherever you want them, configured by the code on either side. We'll walk through all of it below.
Workers Cache is available today to every Worker on any plan, enabled in Wrangler.
This is the caching API we've always wanted Workers to have. Here's why it took us this long, what becomes possible because of it, and what's coming next.
Why server-rendered apps need a cache in front
When we introduced Workers in 2017, the pitch was that you could run code on Cloudflare's network to transform requests on their way to your origin. The Worker sat in front of the cache and the origin:
image
This was the right model for the use cases we were targeting. If you wanted to add a header to every request, rewrite a URL, do an A/B split, or filter traffic before it reached your origin, putting the Worker in front of the cache and the origin gave you full control over what got cached and what didn't. Customers built incredible things with it.
But the world changed. Workers stopped being a thing you bolted onto an origin and started being the origin. Frameworks like Astro, TanStack Start, Next.js, Remix, and SvelteKit all ship a Cloudflare adapter that builds your app as a Worker. There's no origin behind them. The Worker is the server.
When the Worker is the origin, the original architecture has nothing to cache. Every request runs your code, even when the response would be byte-for-byte identical to the one you returned a second ago. The Workers runtime is fast enough that this works — it routinely handles tens of millions of requests per second without breaking a sweat — but "fast enough to render every request" still costs you latency on every page load and CPU time on every invocation. And on a server-rendered app, every page load is, by definition, a render.
Workers Cache flips the architecture. Cloudflare's cache now sits in front of the Worker:
image
On a cache hit, your Worker doesn't run at all. Cloudflare returns the cached response and your CPU billing stays at zero. On a miss, your Worker runs once, populates the cache, and the next request — from anywhere — gets served from cache without invoking your code.
This is what was missing for server-side rendering on Workers. You used to have to choose between two unsatisfying options:
Prerender everything at build time ("static site generation"). Fast page loads, but every change requires a full rebuild and redeploy. For a docs site with a few thousand pages, that's 5–10 minutes. For a large e-commerce site, it's worse — and the build runs every single time you touch anything.
Render every page on every request. Up-to-date content, but every page load pays the rendering cost and every visitor pays the latency.
Workers Cache gives you a third option: server-render on demand, cache the rendered response, refresh it on a time-to-live (TTL) you choose. The first request to a new page still renders. Every subsequent request, until the cache expires, is served as if the page were static. When the cache expires, the next request triggers a re-render — and with stale-while-revalidate, even that one doesn't wait.
You get the speed of a static site without the build time, and the freshness of server rendering without the cost. No framework-specific machinery like Incremental Static Regeneration. Just HTTP caching, working the way it was designed to work, in front of code that was designed to be the origin.
stale-while-revalidate is the part that makes it feel instant
The stale-while-revalidate directive tells Cloudflare that when a cached response expires, it's allowed to serve the stale copy immediately while it refreshes the response in the background. Cloudflare shipped full support for stale-while-revalidate earlier this year, and it's the directive that turns "we cache your Worker" into "your Worker's site feels static."
Without it, the first request after a cache entry expires has to wait for the Worker to render the page from scratch. The user sees that latency. With it, the first request after expiration gets the stale page immediately (with a Cf-Cache-Status: UPDATING header), and the Worker runs in the background to refill the cache. Every user, including the one who triggered the refresh, gets a cache-speed response.
image
In practice, this looks like:
export default {
async fetch(request) {
const html = await renderPage(request);
return new Response(html, {
headers: {
"Content-Type": "text/html; charset=utf-8",
// Treat as fresh for 5 minutes; serve stale for up to an hour
// while a background refresh runs.
"Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
},
});
},
};
The mental model that makes this click:
Fresh window (max-age): Cloudflare serves the cached response. Your Worker doesn't run.
Stale window (stale-while-revalidate): Cloudflare serves the cached response. Your Worker runs in the background to refresh it. No user waits.
Outside both windows: Cloudflare runs your Worker to generate a fresh response, and the user waits for that one render.
You pick the windows. For a product catalog that updates every few minutes, max-age=300, stale-while-revalidate=3600 means visitors basically never wait, and your Worker still runs often enough to keep content fresh. For a blog archive that almost never changes, max-age=86400, stale-while-revalidate=2592000 means your Worker runs once a day per page.
The first request to a brand-new page is the only one that pays the full render cost. After that, the page behaves like static output for visitors, while your Worker still owns how the page gets generated.
One URL, many representations: Vary works
Real apps rarely return the same bytes to every client. The same product page might be HTML for a browser and JSON for an API client. The same image might be WebP for clients that support it and JPEG for the ones that don't. The same homepage might come back in English, French, or Japanese depending on the user.
Doing this without a cache is easy — your Worker just reads the request header and returns the right thing. Doing it with a cache is where it usually gets ugly. Most caches give you two bad options: cache nothing on URLs that have multiple representations, or cache one representation and serve it to everyone.
Workers Cache supports the standard HTTP Vary header, which is the right way to solve this. When your Worker returns a response with Vary: Accept-Encoding (or Accept, or Accept-Language, or any other request header), Cloudflare stores a separate cached variant per distinct combination of those headers — and only returns a variant whose stored values match the incoming request.
export default {
async fetch(request) {
const accept = request.headers.get("Accept") ?? "";
const wantsWebp = accept.includes("image/webp");
const body = wantsWebp ? await fetchWebpImage() : await fetchJpegImage();
return new Response(body, {
headers: {
"Content-Type": wantsWebp ? "image/webp" : "image/jpeg",
"Cache-Control": "public, max-age=3600",
// Cache a separate variant per distinct Accept header value.
Vary: "Accept",
},
});
},
};
One URL, two cached variants. A browser that sends Accept: image/webp,*/* gets the WebP. A browser that sends Accept: image/jpeg gets the JPEG. Both come from cache. Your Worker writes both variants on the first request to each, and then runs zero times for either after that.
This is the well-trodden HTTP standard for content negotiation, and Workers Cache implements it the way RFC 9110 and RFC 9111 describe. There's no allowlist of what headers you can Vary on. You list whatever you need, and Cloudflare keys variants on the verbatim values. The docs go through the edge cases — how to keep variant fan-out under control by normalizing headers in a gateway Worker, why purges invalidate all variants of a URL together, and the one case (Vary: *) that disables caching entirely.
This is your Worker's cache, not your zone's
Before we get to what becomes possible with all this, there's a conceptual shift worth naming.
Cloudflare has had a cache forever. It's configured at the zone level: Cache Rules, Page Rules, the cached-file-extensions list, Cache Reserve, Tiered Cache topology, custom cache keys. All of it is set per zone, and historically a Worker had to either fit into that zone's configuration or work around it.
Workers Cache is different. It's your Worker's cache — it belongs to the Worker, not to a zone. This has a bunch of consequences that turn out to matter:
There is no zone configuration to manage. Cache Rules, cache level settings, the file-extensions list, Page Rules — none of them apply to Workers Cache. The Worker's Cache-Control headers are the configuration.
The cache follows the Worker, not the hostname. A Worker that's bound to api.example.com, api.example.net, and invoked over a service binding shares one cache across all three. A request to /users/42 hits the same cached entry regardless of which way in it came.
The cache works on workers.dev. It works in preview URLs (each preview gets its own cache, so testing a change doesn't poison production). It works in Workers for Platforms (each user Worker has its own cache, isolated from the dispatcher and from other tenants). All of these used to be second-class citizens for caching. They aren't anymore.
Purges are scoped to the Worker’s entrypoint. When you call ctx.cache.purge({ purgeEverything: true }), you're only purging your Worker entrypoint's cache. No risk of nuking your zone's other content. No risk of one Worker's deploy invalidating another's data.
What you configure about caching, you configure in code: which paths get longer TTLs (branch on the path and set a different max-age), which requests bypass the cache (return Cache-Control: private), how the cache key is shaped (control what gets into ctx.props, normalize the URL in a gateway Worker before dispatching). The Worker you already wrote is the configuration surface.
The full docs go deep on this in Workers Cache: your Worker's cache.
Two tiers, every Worker, no configuration
Workers Cache is regionally tiered by default. There are two layers:
A lower tier in the Cloudflare data center closest to the user. Every data center that receives traffic for your Worker has its own lower-tier cache.
An upper tier that aggregates fills across the whole network. There are fewer of these, and every lower tier consults the upper tier on a miss.
A request hits the lower tier first. On a hit, the response is served and that's the end of it. On a miss, the lower tier asks the upper tier. On a hit there, the response is returned and also stored in the lower tier on the way back. Only if both tiers miss does your Worker actually run — and the response from that run gets stored in both tiers.
image
The reason this matters is that the first request anywhere in the world populates the upper tier. Every subsequent request, from any data center, can be served from the upper tier without your Worker running — even if the lower tier at that data center has never seen the request before. Cache hit ratios are dramatically higher than they would be with a single flat cache layer, which is exactly what you want when your Worker is the origin.
This is the same topology that powers Tiered Cache for zones today, except you don't configure it. There is no dialog for "turn on tiered cache for my Worker." Every Worker that has caching enabled gets tiering for free.
If your Worker uses Smart Placement, the cache composes cleanly with it: tiers are consulted first, and only if both miss does Smart Placement route execution close to your origin. We have more to say about how those layers interact, including a few rough edges we're planning to smooth out, in the docs.
Run your app near the user and near the data
There's a recurring tension in web performance that nobody has fully resolved: you want your code to run close to the user (because the round-trip between user and server is on the critical path), and you want your code to run close to the data (because every database query is also a round-trip). Pick one, and the other gets slow.
We've spent years chasing both. Our network puts us within ~50ms of about 95% of the world's Internet users. Smart Placement and Placement Hints let you keep your code close to your data without ever having to think about cloud regions. But until now, the two pieces didn't fully compose. You could do "near the user" or "near the data," and if you wanted both halves of your app to be in the right place at the same time, you had to be a Cloudflare expert. We knew we could do better.
Workers Cache is the piece that closes the gap. Because the cache belongs to the Worker (not the zone), and because service bindings and ctx.exports calls between Workers go through the cache, you can build an app as a chain of Workers — each one running where it should run — with the cache as the seam between them.
The architecture looks like this:
image
Worker A runs near the user. It handles the cheap, latency-sensitive parts of every request: authentication, rate limiting, routing, header normalization, rendering the outer "shell" of an HTML page that doesn't depend on data.
Worker B runs near the data, courtesy of Smart Placement or an explicit Placement Hint. It does the heavy work: server-rendering pages that fetch data, reading product catalogs, generating search results, aggregating APIs, expensive transforms.
Workers Cache sits in front of Worker B. When Worker A calls Worker B over a service binding, Cloudflare checks Worker B's cache first. On a hit, Worker A receives the response and Worker B doesn't run at all — no data-center hop, no database query, no rendering work.
The cache hit path becomes: user → Worker A near the user → cache hit for Worker B → response. The data hop is paid only on a miss. Your hot pages run at the speed of code-in-front-of-the-user, and your cold pages still benefit from running near the data when they do execute.
You don't have to architect anything special to get this. Write your app as two Workers, point one at the other with a service binding, turn caching on in Worker B’s wrangler.jsonc file, and you're done.
image
Multi-tenant by default, with ctx.props
If you're caching a Worker that returns user-specific data — say, an API that serves different content per logged-in user — you need a way to make sure one user can never see another user's cached response. The standard solution is "don't cache authenticated requests," and Cloudflare's automatic bypass for Authorization headers does exactly that. But "don't cache anything" gives up the entire performance win.
Workers Cache solves this by making the caller's ctx.props part of the cache key. When one Worker calls another over a service binding and passes ctx.props with a user ID, tenant ID, or any other identifier, callers with different props get separate cache entries. One user's response can never leak into another user's cache.
import { WorkerEntrypoint } from "cloudflare:workers";
interface Props { userId: string; }
export default class Backend extends WorkerEntrypoint<Env, Props> {
async fetch(request: Request): Promise<Response> {
// ctx.props.userId is part of the cache key. User A and User B
// requesting the same URL get separate cached entries.
const { userId } = this.ctx.props;
const data = await loadUserData(userId);
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=300",
},
});
}
}
The typical pattern is to authenticate the request in a gateway Worker, strip the Authorization header, set the authenticated user's ID into ctx.props, and then call the cached backend Worker. The gateway runs on every request (it has to, to authenticate), but the expensive backend only runs when there's no cache entry for that user yet. Auth'd APIs go from "uncacheable" to "cached per user with full safety," and the cache key does the isolation for you. The docs walk through this in detail in M
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み