メインコンテンツへスキップ

OpenNextとCloudflare Workersのキャッシュ経路とビルド順序

OpenNextでCloudflare Workersへ配信する際の、Static Assets・Worker・Workers Cacheの経路を解説します。run_worker_firstの設定、ビルド順序、deploy後のpurge境界を本番検証から整理します。

(更新日:)9分で読めます
OpenNextとCloudflare Workersのキャッシュ経路とビルド順序

Workers Cache再導入前、このサイトには実際のリクエスト経路から参照されないworker-cache.jsと、対応していないWranglerの[cache]設定が同居していました。

設定の存在を確認するtestまであったのですが、activeなentrypointにはつながっていません。

設定ファイルだけを見て、cacheを実装したつもりになっていました。

2026年7月21日、entrypointをworker-cache.jsへ切り替え、.open-next/worker.jsへ委譲する構成で本番試験を始めました。7月30日のversion境界試験では旧versionのresponseが新versionから再利用されたため、いったんWorkers Cacheを無効化しました。

翌31日、CloudflareのWorkers Caching purge仕様を確認し直し、原因を切り分けました。zone cacheのpurgeではWorkers Cacheは消えません。そこでcache自体は有効へ戻し、deploy後にzone、Workers Cache、zoneの順でpurgeする構成へ改めました。最後にzoneをもう一度消すのは、最初のzone purgeとWorkers Cache purgeの間に旧responseが入り直す余地を閉じるためです。

そこで、SSG-firstの設計記事で触れた失敗を整理し、entrypointからビルド成果物まで実際につながる経路を検証しました。

runworkerfirstをfalseのままにした理由

run_worker_firstは、mainに指定したWorkerの種類ではなく、静的assetとWorkerのどちらを先に実行するかを決める設定です。CloudflareのWorker script routingでは、既定のasset-first構成は一致する静的assetを先に返し、見つからない場合にWorkerを呼びます。OpenNextのStatic Assetsガイドでも、未指定時はfalseであることが説明されています。

現在のwrangler.tomlrun_worker_firstを指定していないため、既定値のfalseです。

設定静的assetとWorkerの順序向いている要件
run_worker_first = false一致するassetを先に返し、なければWorkerを呼ぶ公開assetをWorkerの処理から分離したい
run_worker_first = trueassetを返す前にすべてのrequestでWorkerを呼ぶ認証、変換、middleware、Next.jsのrewriteが必要
path patternの配列指定pathだけWorkerを先に呼び、他はasset-firstOAuth callbackなど一部の経路だけ前処理したい

このサイトでは、Pagefind index、OGP、font、public配下のassetへ認証やrequest単位の変換をかけません。これらはasset-firstで配信し、assetとして一致しないrequestだけをworker-cache.jsからOpenNext Workerへ渡します。全assetをwrapperへ通す理由がないため、run_worker_firstfalseのままにしています。

反対に、静的assetにも認証、logging、HTMLRewriter、Next.js middlewareやrewriteを適用する場合はtrueが候補になります。OpenNextのskew protectionを使う場合もtrueが必要です。ここは性能上の好みではなく、静的assetより前にWorker処理が必要かどうかで決めます。

現在のリクエスト経路を、Static Assetsの判定とWorker内部のcacheへ分けると次の形です。

Visual

Static AssetsとOpenNext Workerのリクエスト経路

run_worker_firstを省略したasset-first構成では、一致する静的assetはWorkerを通らず返ります。assetに一致しないリクエストだけがworker-cache.jsを経由してOpenNext Workerへ進み、OpenNextは配信用にコピーされたread-only cache assetsを参照します。

asset-firstの判定とactive entrypoint以降のcache境界

Static AssetsとOpenNext Workerのリクエスト経路

図の読み方

  1. Workers Static Assetsが、リクエストに一致する静的assetの有無をWorkerより先に判定します。
  2. 一致するPagefind・OGP・fontなどは、worker-cache.jsを通らずに返ります。
  3. 一致しないリクエストだけがactive entrypointのworker-cache.jsへ入り、Workers CacheのHITならそこで返ります。
  4. MISSまたはBYPASSではOpenNext Workerへ委譲し、ビルド後にコピーしたread-only cache assetsか3本のrequest-time APIへ進みます。

Workers Cache導入前はOpenNext Workerを直接entrypointにした

OpenNext Workerを直接entrypointにしていたこの記事の公開時点では、wrangler.tomlのentrypointをOpenNextが生成する.open-next/worker.jsへ直接向けていました。間に独自wrapperは挟んでいませんでした。以下は当時の設定例です。

toml
main = ".open-next/worker.js"
workers_dev = false

[assets]
directory = ".open-next/assets"
binding = "ASSETS"

この時点でもrun_worker_firstを指定せず、既定値はfalseでした。public由来の静的assetはリクエストごとにWorkerを通さずWorkers Static Assetsから配信でき、asset binding自体の挙動はCloudflare公式でも確認できます。

ここで確認できたのは、当時のwrangler.tomlのentrypointとasset bindingです。Next.jsの各pageがrequest時にどの経路を通ったかまで、この設定だけから証明することはできません。

現在のentrypointはworker-cache.js

現在のwrangler.tomlは、worker-cache.jsをactive entrypointにします。静的assetに一致せずWorkerへ到達したrequestはOpenNextが生成したWorkerへ委譲し、cache対象外responseの保護とdeploy後のpurgeをwrapperが受け持ちます。

toml
main = "worker-cache.js"
workers_dev = false

[cache]
enabled = true
cross_version_cache = false

[assets]
directory = ".open-next/assets"
binding = "ASSETS"

run_worker_firstはここでも省略し、既定値のfalseを使っています。entrypointを独自wrapperへ変えたことを、全静的assetをWorkerへ通す設定変更にはしていません。

本番試験中はcross_version_cache = falseを設定していました。しかし、zone cacheをpurgeして新versionへ切り替えた後も、旧versionで付けたmarkerがcache対象responseに残りました。同じNRT PoPのno-store responseは新版markerを返していたため、単なるdeploy伝播ではありませんでした。

ここで見落としていたのがpurgeの境界です。Cloudflare公式の説明では、Dashboardやzone APIのpurgeはWorkers Cachingのcontentへ影響しないと明記されています。7月30日に実行したzone purgeだけでは、Workers Cacheを削除したことになりません。

cross_version_cache = falseは防御として残しました。その上でdeploy後、専用endpointからzone cacheをpurgeしてtokenの権限を確かめ、ctx.cache.purge({purgeEverything:true})を呼び、最後にzone cacheをもう一度purgeします。どこか1つでも失敗すればdeploy処理を成功扱いにしません。

正しい削除手段をdeploy経路へ入れた上でWorkers Cacheを使う。今回の是正はそこです。

更新をdeploy単位にしたからread-onlyを選んだ

このサイトは、記事の更新をビルドとデプロイの単位で扱います。リクエスト時にcacheを書き換える要件がないため、SSGルートのincremental cacheにはOpenNextが説明しているread-onlyのStatic Assets実装を選びました。

ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config";
import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";

export default defineCloudflareConfig({
  incrementalCache: staticAssetsIncrementalCache,
});

OpenNext buildはcacheを.open-next/cache/<buildId>へ出します。しかし、asset bindingが配信するのは.open-next/assetsです。出力しただけでは届きません。

scripts/copy-cache-to-assets.mjsで、cacheを.open-next/assets/cdn-cgi/_next_cache/<buildId>へコピーします。

見るべきだったのはcache設定の有無ではなく、ビルド後の生成物がasset bindingの配下へ届いたかどうかでした。

staticAssetsIncrementalCacheはread-onlyで、revalidationには対応しません。時間ベースのrevalidationが必要になればR2 incremental cacheとQueue、on-demand revalidationが必要になればTag Cacheを含め、要件に応じた書き込み可能な構成へ切り替えます。

Pagefindがcache assetsと同じbuildに入る順序

Pagefindは静的HTMLを入力にするため、next buildの後にしか実行できません。一方、OpenNextが配信用assetをバンドルした後にPagefindを作っても、検索indexは.open-next/assetsへ入りません。

つまり、Pagefindを実行できる場所はnext buildの後かつOpenNext buildの前です。

現在の順序は次の通りです。

MermaidOpenNext production build pipelineprebuildからWrangler bundle gateまでの直列build工程

production buildは生成、Next.js build、artifact検証、PagefindとAEO生成、OpenNext変換の順で進みます。最後にcache assetsのコピーとWrangler dry-runを通すことで、配信bundleへ進む前に不整合を止めます。

  1. prebuildでllms、OGP font、静的OGPを生成してからnext buildを実行します。
  2. HTML、CSS、SEO artifactsを検証し、Pagefind indexとAEO artifactsを生成します。
  3. OpenNext buildとcache assets copyの後にWrangler dry-runとbundle gateを実行します。

package.jsonでは、build:allがこの責任を直列にしています。

json
{
  "build": "next build",
  "postbuild": "... && pagefind --site .next ... && ...",
  "build:all": "pnpm build && opennextjs-cloudflare build --skipNextBuild && node scripts/copy-cache-to-assets.mjs && pnpm validate:worker-boundary",
  "build:check-size": "pnpm build:all && node scripts/check-bundle-size.js"
}

--skipNextBuildでNext.jsのビルドをやり直さないのも、このためです。検証とPagefindまで終えた同じNext.js成果物を、そのままOpenNextへ渡します。

testで本番レスポンスまで証明しない

ソースコードで検査するのは、17 routeのdynamicParams = falseworker-cache.jsがactive entrypointであること、Workers Cacheの[cache]設定、purge endpoint、そしてdeploy後にpurgeとcache検証が並ぶ順序です。ここでは、コードに書かれた契約までを見ます。

ビルド成果物では、同じ成果物にPagefind・静的OGP・font subset・SSG cache assetsが入ったかを確認し、Wrangler dry-runまで通します。Worker bundleとStatic Assetsは同じ容量に丸めません。Cloudflare Workersの制限とプロジェクト側の運用閾値を分け、bundleと配信assetを別々に計測します。SSG-firstで増えるasset側には、上限ではなく構成を見直すための閾値を置いています。

ここで確認できるのは、デプロイ前のソースコードとビルド成果物までです。本番の外部レスポンスを確認できたことにはなりません。

デプロイ後のworkers.devには外部からHTTPリクエストを送り、duplicate hostとしてindexableな本番HTMLを返していないかを確認します。probeの結果はsafe・blocker・inconclusiveに分け、Accessやerror responseで判断できない場合はinconclusiveです。応答が返っただけでsafeにはしません。

検証地点確認する証拠証明できないこと
ソースコード17 route、entrypoint、[cache]、purge順序実際のビルド成果物
ビルド成果物Pagefind・OGP・font・SSG cache assets、Wrangler dry-run本番edgeのレスポンス
デプロイ後expected host、status、cache header、version markerソースコード上の意図

設定ではなく経路を残す

この構成を選べるのは、検索を静的indexとbrowserで完結させ、request-time処理を少数のAPIへ隔離し、更新をdeployまで待てるサイトだからです。ユーザーごとに内容が変わる、request時のDB queryや認証後の個別画面が主要機能になる、価格や在庫の更新をdeployまで待てないといった要件には合いません。

大量コンテンツでfull buildとcache assetsが増え続ける場合も、そのままでよいとは思っていません。ISRやon-demand revalidationが要件になればSSGを併用しつつ、read-only cacheからR2 incremental cache、Queue、Tag Cacheを要件に応じて組み合わせる構成へ切り替えます。

今回の失敗で、設定とtestがあるだけでは実行経路の証明にならないと分かりました。

それ以来、entrypoint・ビルド成果物・asset binding・デプロイ後の外部レスポンスを分け、どこまで確認できたかを残しています。現在のentrypointはworker-cache.jsで、Workers Cacheは有効です。deploy後の全削除とMISSからHITへの再充填確認までを、公開の条件にしています。

ここまで追えて、やっと実装したと言えると思っています。

Written by

柿添貴士(TakashiKakizoe) profile photo

柿添貴士(TakashiKakizoe)

Webエンジニア/テックリード

Fukuoka, Japan

  • PHP
  • Laravel
  • Next.js
  • AWS
  • Cloudflare
  • AI Agent