- Workers の実行モデル — V8 isolate がコールドスタートをほぼ無くす仕組みと、リージョンという概念がない理由
- ES Modules 形式の fetch ハンドラと、Request / Response / URL オブジェクトの扱い
wrangler devでのローカル開発 →wrangler deploy→ workers.dev URL での動作確認という基本サイクル- fetch() サブリクエストと HTMLRewriter という代表的な Runtime API の入口
- TypeScript での開発と
wrangler typesによる型生成、wrangler tailでのログ確認
Workers の実行モデル — V8 isolate とコールドスタート
Cloudflare Workers のランタイムは、Chromium や Node.js と同じ V8 エンジンの上に構築されています。ただしコードが動く場所が根本的に異なります。あなたの Worker は 1 台のサーバーではなく、数百拠点・数千台のマシンからなる Cloudflare のグローバルネットワーク全体に配布され、リクエストを受け取ったデータセンターがそのまま実行します。「どのリージョンにデプロイするか」を選ぶ設定は存在しません。
従来のサーバーレス(コンテナ型)との最大の違いが isolate(アイソレート) です。isolate は V8 が管理する軽量な実行コンテキストで、コードごとにメモリが完全に分離されたサンドボックスとして働きます。関数ごとに仮想マシンやコンテナを起動する代わりに、すでに起動済みのランタイム内へ isolate を作るため、仮想マシンモデルで発生していたコールドスタートが排除されます。ドキュメントによれば、isolate の起動はコンテナ上の Node プロセスより約 100 倍高速で、起動時のメモリ消費は 1 桁少なくなります。
| 観点 | コンテナ型サーバーレス | Workers(V8 isolate) |
|---|---|---|
| 起動単位 | 関数ごとに言語ランタイムを含むプロセスを起動 | 起動済みランタイム内に isolate を作成 |
| コールドスタート | 発生する | 仮想マシンモデル由来のものは発生しない |
| ランタイムのオーバーヘッド | プロセスごとに支払う | コンテナ起動時に一度だけ支払う |
| デプロイ先 | 選択したリージョン | グローバルネットワーク全体 |
リクエストが処理される流れは次のとおりです。
- ユーザーが
*.workers.devまたは Cloudflare 管理下のドメインへ HTTP リクエストを送る - Cloudflare のいずれかのデータセンターがリクエストを受け取る
- そのマシン上のランタイムが isolate 内であなたの Worker の
fetch()ハンドラを呼び出す - ハンドラが返した
Responseがユーザーへ返る
注意すべき性質もあります。isolate はマシンのリソース逼迫などで破棄(evict)されることがあり、また同じ 2 つのリクエストが同じインスタンスに届く保証もありません。したがってグローバル変数に可変状態を保存しないことが公式に推奨されています。またセキュリティ上の理由で eval() や new Function は使えません。
fetch ハンドラと Request / Response / URL
Worker のコードは ES Modules 形式で書きます。オブジェクトを export default し、処理したいイベントに対応するハンドラをプロパティとして持たせます。HTTP リクエストを処理するのが fetch() ハンドラです。
export default {
async fetch(request, env, ctx): Promise<Response> {
return new Response("Hello World!");
},
} satisfies ExportedHandler<Env>;
fetch ハンドラには常に 3 つの引数が渡されます。
| 引数 | 型 | 役割 |
|---|---|---|
request | Request | 受信した HTTP リクエスト |
env | object | Worker に紐付いたバインディング(KV や環境変数など。第 3 章で詳説) |
ctx | Context | ctx.waitUntil() と ctx.passThroughOnException() を提供 |
request と Response がブラウザとの往復、env はバインディング経由で外部リソースへ、ctx だけが破線の側 — レスポンスを返した後も続く処理 — を担当します。Request と Response は Fetch API 標準のインターフェースです。押さえておくべき Workers 固有のポイントは次の 3 点です。
- 受信 Request は不変(immutable)。プロパティを変更したいときは
new Request(url, request)で新しいオブジェクトを作ります。 request.cfオブジェクトに Cloudflare ネットワークが付与した情報が入ります。データセンターの IATA コードcolo、国コードcountry、都市city、タイムゾーンtimezoneなどが読み取れます(ダッシュボードのプレビューや Playground では利用不可)。- ボディの読み取りは
request.json()/request.text()/request.formData()/request.arrayBuffer()の Promise ベース API で行います。
ルーティングの基本は、標準の URL API で request.url を分解することです。new URL(request.url) の pathname や searchParams で分岐します。パターンマッチには URLPattern API も利用できます。なお、Worker 内から外部へ HTTP リクエストを送る fetch() API(サブリクエスト)はハンドラの内側(Request コンテキスト)でしか呼べません。グローバルスコープで呼ぶと例外になります。
デプロイした Worker を公開する経路(Routes and domains)は 3 種類あります。本章では最も手軽な workers.dev を使います。
- workers.dev — Worker 作成時に自動で割り当てられる
<Worker 名>.<アカウントのサブドメイン>.workers.devという URL。個人・ホビー用途向けで、すぐ試せる - Custom Domains — 自分のドメインを Worker をオリジンとして接続する。本番運用の推奨
- Routes — Cloudflare ゾーン内の URL パターンに Worker を割り当てる。既存オリジンサーバーの前段に置く用途
TypeScript での開発
TypeScript は Workers の第一級言語です。全 API の型定義は、オープンソースのランタイム workerd から直接生成されています。正しい型はあなたの Worker の compatibility date・compatibility flags・バインディング構成に依存するため、公式の推奨は wrangler types コマンドでプロジェクトの設定に合わせて型を動的生成することです。
npx wrangler types
既定で worker-configuration.d.ts が生成され、バインディングに基づく Env 型とランタイム型の両方が含まれます。生成したファイルは tsconfig.json の compilerOptions.types に追加します。
{
"compilerOptions": {
"types": ["./worker-configuration.d.ts"]
}
}
設定ファイルを変更したら wrangler types を再実行します。CI では npx wrangler types --check でコミット済みの型が最新かを検証できます。ハンドラ本体には satisfies ExportedHandler<Env> を付けると、引数と戻り値の型が推論されつつ検査されます。
ハンズオン
以降の 3 本はいずれも「編集 → wrangler dev → wrangler deploy → wrangler tail」という同じ一周をなぞります。どの操作が自分のマシンで完結し、どこで Cloudflare 側へ出ていくのかを先に押さえておきます。
wrangler deploy のアップロード、左へ戻るのは wrangler tail が受け取るログだけで、編集と動作確認は自分のマシンの中で完結します。request.cf のようにエッジでしか付かない情報を確かめたいときだけ、右側へ一周する必要があります。プロジェクトの作成からローカル実行、世界へのデプロイまでの基本サイクルを一周します。所要 10 分。Free プランで完結します。
- C3(create-cloudflare-cli)でプロジェクトを作成します。
対話プロンプトでは Hello World example → Worker only → 言語は TypeScript を選びます。「Do you want to use git for version control?」は Yes、最後の「Do you want to deploy your application?」は No にします。npm create cloudflare@latest -- my-first-worker cd my-first-workerで移動し、生成物を確認します。wrangler.jsonc(設定ファイル)、src/index.ts(Worker 本体)が中心です。{ "name": "my-first-worker", "main": "src/index.ts", "compatibility_date": "2026-08-10" }- ローカル開発サーバーを起動します。初回は Cloudflare アカウントへのログインがブラウザで求められます。
ブラウザでnpx wrangler devhttp://localhost:8787を開くとHello World!が表示されます。 src/index.tsの返却文字列を"Hello Worker!"に書き換えて保存し、ブラウザをリロードして表示が変わることを確認します。export default { async fetch(request, env, ctx): Promise<Response> { return new Response("Hello Worker!"); }, } satisfies ExportedHandler<Env>;- デプロイします。サブドメイン未設定の場合はプロンプトが案内してくれます。
npx wrangler deploy
出力に表示される https://my-first-worker.<YOUR_SUBDOMAIN>.workers.dev(<YOUR_SUBDOMAIN> はアカウントごとの workers.dev サブドメイン名)へアクセスし、Hello Worker! が世界中どこからでも返ることを確認できれば成功です。
URL API でパスとメソッドによる分岐を実装し、小さな JSON API を作ります。所要 10 分。2-1 のプロジェクトをそのまま使います。
src/index.tsを次の内容に置き換えます。export default { async fetch(request, env, ctx): Promise<Response> { const url = new URL(request.url); if (url.pathname === "/") { return new Response("トップページです"); } if (url.pathname === "/api/geo" && request.method === "GET") { const body = JSON.stringify({ colo: request.cf?.colo, country: request.cf?.country, city: request.cf?.city, }); return new Response(body, { headers: { "content-type": "application/json; charset=utf-8" }, }); } if (url.pathname.startsWith("/hello/")) { const name = decodeURIComponent(url.pathname.slice("/hello/".length)); return new Response(`Hello, ${name}!`); } return new Response("Not Found", { status: 404 }); }, } satisfies ExportedHandler<Env>;npx wrangler deployで再デプロイし、curl で各ルートを叩きます。curl https://my-first-worker.<YOUR_SUBDOMAIN>.workers.dev/api/geo curl https://my-first-worker.<YOUR_SUBDOMAIN>.workers.dev/hello/Cloudflare curl -i https://my-first-worker.<YOUR_SUBDOMAIN>.workers.dev/nothing
/api/geo は {"colo":"NRT","country":"JP","city":"Tokyo"} のような、リクエストが着地したデータセンターと発信元の情報を返します(値は接続元により変わります)。/hello/Cloudflare は Hello, Cloudflare!、未定義パスは 404 を返せば成功です。request.cf の情報は Cloudflare のグローバルネットワークが付与するものなので、デプロイ後の workers.dev URL で確認するのがポイントです。
fetch() サブリクエストで外部ページを取得し、HTMLRewriter でストリーミング変換します。デプロイ後は wrangler tail でログを観測します。所要 15 分。
src/index.tsを次の内容に置き換えます。HTMLRewriter は CSS セレクタで要素ハンドラを登録し、レスポンスを丸ごとバッファせずに変換します。class TitleHandler { element(element: Element) { element.setInnerContent("Workers 教科書で書き換えたタイトル"); } } export default { async fetch(request, env, ctx): Promise<Response> { const upstream = await fetch("https://developers.cloudflare.com/"); console.log(`upstream status: ${upstream.status}`); return new HTMLRewriter() .on("title", new TitleHandler()) .transform(upstream); }, } satisfies ExportedHandler<Env>;npx wrangler deployでデプロイします。- 別のターミナルでログの購読を開始します。
npx wrangler tail - ブラウザで workers.dev URL を開きます。Cloudflare docs のページが表示され、ブラウザのタブタイトルだけが書き換わっています。
tail 側のターミナルには、リクエストごとに console.log の内容と実行結果を含む構造化 JSON("outcome": "ok"、logs 配列に upstream status: 200 など)が流れてくれば成功です。npx wrangler tail | jq .event.request.url のように jq と組み合わせた絞り込みもできます。
つまずきポイント
fetch() などの非同期 API は Request コンテキスト(ハンドラの内側)でのみ使えます。モジュールのトップレベルで const promise = fetch("https://example.com/") のように書くとスクリプト起動時に例外が発生し、Worker 全体が動きません。必ず fetch() ハンドラの中から呼びます。
isolate はリソース逼迫などで破棄されることがあり、2 つのリクエストが同じインスタンスに届く保証もありません。「グローバル変数のカウンタが勝手にリセットされる」「値がリクエスト間で見えたり見えなかったりする」のは正常な挙動です。永続化には KV や Durable Objects(後の章)を使います。
*.workers.dev サブドメインへの初回デプロイ直後は 523 エラーが表示されることがあります。これは 1 分ほど待つと自然に解消すると公式ドキュメントに明記されています。慌てて再デプロイする必要はありません。
トラフィックが多い Worker では tail が sampling mode に入り、一部メッセージが破棄されます。--status error や --method、--search などのフィルタで流量を絞ると回避できます。また Real-time logs はあくまでリアルタイム閲覧用でログは保存されず、同時に閲覧できるクライアントは 2026-08 時点で最大 10 です。保存が必要なら Workers Logs を使います。
理解度チェック
Q1. Workers でコールドスタートがほぼ問題にならないのはなぜ?
関数ごとに仮想マシンやコンテナを起動する代わりに、起動済みの V8 ランタイム内に軽量な isolate を作って実行するためです。isolate の起動はコンテナ上の Node プロセスより約 100 倍高速で、仮想マシンモデル由来のコールドスタートが排除されます。
Q2. fetch ハンドラに渡される 3 つの引数は?
request(受信した HTTP リクエスト)、env(バインディング)、ctx(waitUntil() / passThroughOnException() を持つ Context)です。
Q3. 受信した Request のヘッダーや URL を変更したいときはどうする?
受信 Request は不変(immutable)なので直接変更できません。new Request(url, request) のように既存の Request を元に新しい Request オブジェクトを構築します。
Q4. デプロイした Worker の URL はどんな形式になる?
<Worker 名>.<アカウントのサブドメイン>.workers.dev 形式です。workers.dev は個人・ホビー用途向けの扱いで、本番運用には Custom Domain または Route が推奨されています。
Q5. レスポンス返却後に続けたい処理(ログ送信やキャッシュ書き込み)はどう書く?
ctx.waitUntil(promise) に Promise を渡すと、レスポンス返却後も Worker の寿命が延長されて処理が継続されます。ただし 2026-08 時点で延長は呼び出し終了後最大 30 秒までで、間に合わない処理は Queues の利用が推奨されています。
まとめと次章へ
- Workers は V8 isolate で動き、グローバルネットワーク全体にデプロイされる。リージョン選択は存在せず、コールドスタートもほぼ無い
- コードは ES Modules 形式で
fetch(request, env, ctx)ハンドラを書き、Request を受けて Response を返す。受信 Request は不変で、request.cfに地理情報などが入る - ルーティングは
new URL(request.url)のpathname分岐が基本。外部への fetch() サブリクエストや HTMLRewriter がストリーミングで使える - 開発サイクルは
wrangler dev(localhost:8787)→wrangler deploy→ workers.dev URL 確認 →wrangler tailでログ観測 - TypeScript では
wrangler typesで設定に合った型を生成し、satisfies ExportedHandler<Env>で型検査する
次章では、この章で素通りした第 2 引数 env の中身 — wrangler.jsonc の設定、環境変数とシークレット、KV や R2 につながるバインディング、Cron Triggers による定期実行 — を扱い、Worker を「本物のアプリケーション」に近づけます。
参考リンク: