入ってきたリクエストがWorkerを経由して出る道と、静的アセットとして直接出る道に分かれる流れを表した代表画像

概要

この記事は Cloudflare Workers 静的サイトガイド - デプロイからSEOまで シリーズの一部です。

静的サイトをCloudflareに載せる方法として Workers Static Assets があります。ビルド成果物(dist/)をアップロードすると全世界のエッジでサービングされ、必要なら その前にコードを一枚重ねることができます。

静的ホスティングとして使っているうちに「レスポンスを少し直したい」という要求が生まれたとき、サーバーを新しく立てずに解決できる道です。

この記事で扱うこと

  • Workers Static Assetsの構造 — アセットとWorkerコードが一単位でデプロイされる
  • リクエストがどの順序で流れるのか — いつWorkerが動き、いつ動かないのか(シーケンス図)
  • 課金がどこで発生するのかと無料プランの制限

続きの記事

この記事は構造とデプロイを扱います。残りは別にあります。

KV・R2・D1のような他のバインディング、Durable Objects、Cron Triggersは範囲外です。

前提

Cloudflareのアカウントがあり、静的サイトをビルドできること(npm run builddist/)以外に必要な事前知識はありません。


Workers Static Assets とは

一文で言うと「静的ファイルの束 +(任意の)Workerコード」を一単位でデプロイすることです。

1my-worker
2├─ static assets   dist/**        HTML · JS · CSS · images
3└─ worker code     src/worker.ts  optional

Workerコードを入れなければ、ただの静的ホスティングです。入れれば、リクエストがアセットに届く前または後にコードを挟み込めます。

Pages と何が違うのか
Cloudflare Pagesも静的ホスティングです。ただしCloudflareが新規の静的ホスティングをWorkers側に寄せているため、アカウントによってはダッシュボードにPages作成の導線がまったく表示されないこともあります。新しく始めるならWorkers Static Assetsの方が無難です。

設定ファイル

wrangler.jsonc 一つで定義します。使えるキーは Configuration ドキュメント にすべてあります。

 1{
 2  "name": "my-site",
 3  "compatibility_date": "2026-09-08",
 4
 5  // Optional. Omit for pure static hosting.
 6  "main": "./src/worker.ts",
 7
 8  "assets": {
 9    // Name of env.ASSETS inside the worker
10    "binding": "ASSETS",
11    // Overridable with --assets
12    "directory": "./dist",
13    "not_found_handling": "single-page-application"
14  }
15}

compatibility_date とは何か

ランタイム動作の基準日ドキュメント)です。この日付の動作に固定されます — CloudflareがランタイムをアップデートしてもこのWorkerはそのまま動きます。

言い換えると、日付を上げることが「新しい動作を受け入れる」という意思表示です。デプロイのたびに今日の日付へ自動的に変わってはいけません。同じコードを再デプロイするだけで動作が変わってしまうと、ロールバックがロールバックでなくなります。

上げるときは、日付を直す → ローカルで確認する → デプロイする、という順序で人が意図して上げます。


リクエストはどう流れるのか

ここがこの記事の核心です。いつWorkerが動き、いつ動かないのかを知ってはじめて、課金も動作も理解できます。

基本 — Workerコードがないとき

sequenceDiagram participant U as Browser participant CF as Cloudflare edge participant A as Static assets U->>CF: GET /assets/index-a1b2.js CF->>A: asset exists? A-->>CF: yes CF-->>U: 200 file Note over CF: worker not invoked · not billable

単純です。ファイルがあれば返します。

SPAなら — 存在しないパスをindex.htmlへ

SPAは /products/1234 のようなパスに実際のファイルがありません。ブラウザがJSで描画するからです。そのままにしておくと404です。

not_found_handling: "single-page-application" がこれを解決します。

sequenceDiagram participant U as Browser participant CF as Cloudflare edge participant A as Static assets U->>CF: GET /products/1234 CF->>A: asset exists? A-->>CF: no Note over CF: not_found_handling = single-page-application CF->>A: GET /index.html A-->>CF: index.html CF-->>U: 200 index.html Note over U: JS boots · client router renders

404ではなく200で index.html を返すという点が重要です。404で返すと検索エンジンがインデックスしません。

Workerコードを重ねると

基本ルールは**「アセットがあればWorkerは動かない」**です。Workerはアセットがないときだけ実行されます。

sequenceDiagram participant U as Browser participant CF as Cloudflare edge participant W as Worker participant A as Static assets U->>CF: GET /api/hello CF->>A: asset exists? A-->>CF: no CF->>W: invoke worker W-->>CF: Response CF-->>U: 200 Note over W: billable

ところがこの基本ルールにはがあります。/products/1234 のようなSPAパスでWorkerを動かしたくても、アセットルーティングが先に index.html として処理してしまうためWorkerが動きません。そして /index.html が実際に存在するので、そもそもWorkerを通りません。

run_worker_first — Workerを先に通す

特定のパスでアセットより先にWorkerを実行するよう指定できます。

1{
2  "assets": {
3    "binding": "ASSETS",
4    "not_found_handling": "single-page-application",
5    "run_worker_first": ["/", "/products/*"]
6  }
7}
sequenceDiagram participant U as Browser participant CF as Cloudflare edge participant W as Worker participant A as Static assets U->>CF: GET /products/1234 Note over CF: matches run_worker_first CF->>W: invoke worker first W->>A: env.ASSETS.fetch(request) A-->>W: index.html (SPA fallback) Note over W: rewrite response W-->>CF: rewritten Response CF-->>U: 200

この構造でWorkerはアセットを代わりにサービングするのではなく、受け取って直して送り出します。 env.ASSETS.fetch(request) がその通路です。

全体の判断順序

SPAルーティングのドキュメント に書かれた順序を整理するとこうなります。

flowchart TD A[Request] --> B{run_worker_first
match?} B -->|yes| W[Invoke worker] B -->|no| C{asset exists?} C -->|yes| D[Serve asset
not billable] C -->|no| E{worker script?} E -->|yes| W E -->|no| F{not_found_handling} F -->|single-page-application| G["index.html · 200"] F -->|404-page| H[404 page] F -->|none| I[404]

課金はどこで発生するのか

Billing and limitations の表現が明確です。

Requests are only billable if a Worker script is invoked.

つまり静的アセットのリクエスト自体は課金されません。 JS・CSS・画像をいくらダウンロードされてもリクエスト数にカウントされません。

課金されるのはWorkerが実行されたリクエストだけです。無料プランは1日10万リクエストで、ここにカウントされるのもWorkerが動いたものだけです。

そのため run_worker_first を広く取ると、そのままコストになります。

1// Bad: even /assets/*.js goes through the worker, and becomes billable.
2"run_worker_first": true
3
4// Good: HTML routes only.
5"run_worker_first": ["/", "/products/*"]

無料プランの他の制限も知っておくとよいです(Limits · Pricing)。

無料有料($5/月〜)
リクエスト10万/日1千万/月を含む
CPU時間10ms/リクエストデフォルト30秒
サブリクエスト50/リクエスト10,000/リクエスト
メモリ128MB128MB

CPU 10ms が最もよく引っかかります。ただし名前の通りCPUを実際に使った時間であって、レスポンスにかかった時間ではありません。

CPU time measures how long the CPU spends executing your Worker code. Waiting on network requests (such as fetch() calls, KV reads, or database queries) does not count toward CPU time.

Cloudflare Workers · Limits · CPU time

await fetch(...) でオリジンを待つ時間は上限にカウントされない、という意味です。HTTPリクエストにはduration制限も別途ないので、オリジンが300msかかってもそれ自体は問題になりません。

ヘッダーを直したりタグを挟み込んだりする程度のWorkerなら、CPUはほとんど使いません。サーバーレンダリングのように計算が入るときだけ際どくなります。実際の値が気になるなら推測せず Monitoring CPU usage を見ましょう — Workers Logsのinvocation logにCPU timeとwall timeが並んで記録されます。


まとめ

  • Workers Static Assets は静的ファイルの束とWorkerコードを一単位でデプロイするものです。
  • 基本ルールは**「アセットがあればWorkerは動かない」**。アセットのパスでWorkerを通したいなら run_worker_first で指定します。
  • 静的アセットのリクエストは課金されません。 Workerが実行されたリクエストだけがカウントされます。そのため run_worker_first を狭く取ることがそのままコストになります。
  • CPU 10ms はレスポンス時間ではなくCPUを使った時間です。APIの待ち時間はカウントされません。

次の記事

参考

Static Assets

設定

制限