Cloudflare ハンズオン教科書|2026-08 版
第 2 章

Workers 基礎 — 初めてのサーバーレス関数

fetch ハンドラを書き、wrangler dev で動かし、世界へデプロイする

この章で学ぶこと

Workers の実行モデル — V8 isolate とコールドスタート

Cloudflare Workers のランタイムは、Chromium や Node.js と同じ V8 エンジンの上に構築されています。ただしコードが動く場所が根本的に異なります。あなたの Worker は 1 台のサーバーではなく、数百拠点・数千台のマシンからなる Cloudflare のグローバルネットワーク全体に配布され、リクエストを受け取ったデータセンターがそのまま実行します。「どのリージョンにデプロイするか」を選ぶ設定は存在しません。

従来のサーバーレス(コンテナ型)との最大の違いが isolate(アイソレート) です。isolate は V8 が管理する軽量な実行コンテキストで、コードごとにメモリが完全に分離されたサンドボックスとして働きます。関数ごとに仮想マシンやコンテナを起動する代わりに、すでに起動済みのランタイム内へ isolate を作るため、仮想マシンモデルで発生していたコールドスタートが排除されます。ドキュメントによれば、isolate の起動はコンテナ上の Node プロセスより約 100 倍高速で、起動時のメモリ消費は 1 桁少なくなります。

リクエスト到着 起動を 2 段待つ=コールドスタート コンテナ型 コンテナを起動 ランタイムを起動 関数を実行 応答 待つ段が無い Workers isolate 作成 Worker を実行 応答 経過時間
図 2-1 読み取るのは 2 本のレーンで実行が始まる位置の差です。コンテナ型はコンテナと言語ランタイムの起動を挟むぶん実行開始が右へずれ、その待ちがコールドスタートとして現れます。Workers はランタイムが常駐しているので増えるのは isolate 1 個だけで、その段ごと消えます(横幅は経過時間の相対イメージで実測値ではありません。isolate の起動はコンテナ上の Node プロセスより約 100 倍高速です)。
観点コンテナ型サーバーレスWorkers(V8 isolate)
起動単位関数ごとに言語ランタイムを含むプロセスを起動起動済みランタイム内に isolate を作成
コールドスタート発生する仮想マシンモデル由来のものは発生しない
ランタイムのオーバーヘッドプロセスごとに支払うコンテナ起動時に一度だけ支払う
デプロイ先選択したリージョングローバルネットワーク全体

リクエストが処理される流れは次のとおりです。

  1. ユーザーが *.workers.dev または Cloudflare 管理下のドメインへ HTTP リクエストを送る
  2. Cloudflare のいずれかのデータセンターがリクエストを受け取る
  3. そのマシン上のランタイムが isolate 内であなたの Worker の fetch() ハンドラを呼び出す
  4. ハンドラが返した 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 つの引数が渡されます。

引数型役割
requestRequest受信した HTTP リクエスト
envobjectWorker に紐付いたバインディング(KV や環境変数など。第 3 章で詳説)
ctxContextctx.waitUntil() と ctx.passThroughOnException() を提供
Cloudflare のエッジ ブラウザ fetch ハンドラ (request, env, ctx) ① Request ② Response env.MY_KV バインディング KV / R2 / Secrets ctx.waitUntil() 返却後も継続 最大 30 秒
図 2-2 3 つの引数は行き先が違います。request と Response がブラウザとの往復、env はバインディング経由で外部リソースへ、ctx だけが破線の側 — レスポンスを返した後も続く処理 — を担当します。

Request と Response は Fetch API 標準のインターフェースです。押さえておくべき Workers 固有のポイントは次の 3 点です。

ルーティングの基本は、標準の URL API で request.url を分解することです。new URL(request.url) の pathname や searchParams で分岐します。パターンマッチには URLPattern API も利用できます。なお、Worker 内から外部へ HTTP リクエストを送る fetch() API(サブリクエスト)はハンドラの内側(Request コンテキスト)でしか呼べません。グローバルスコープで呼ぶと例外になります。

デプロイした Worker を公開する経路(Routes and domains)は 3 種類あります。本章では最も手軽な workers.dev を使います。

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 側へ出ていくのかを先に押さえておきます。

自分のマシン Cloudflare のエッジ コードを編集 src/index.ts 保存で即反映 wrangler dev localhost:8787 wrangler deploy エッジで公開 workers.dev ログが流れる wrangler tail ログを観測 直して再デプロイ
図 2-3 読み取るのは破線をまたぐ辺が 2 本しか無いことです。右へ渡るのは wrangler deploy のアップロード、左へ戻るのは wrangler tail が受け取るログだけで、編集と動作確認は自分のマシンの中で完結します。request.cf のようにエッジでしか付かない情報を確かめたいときだけ、右側へ一周する必要があります。
ハンズオン 2-1|Hello Worker をデプロイする

プロジェクトの作成からローカル実行、世界へのデプロイまでの基本サイクルを一周します。所要 10 分。Free プランで完結します。

  1. C3(create-cloudflare-cli)でプロジェクトを作成します。
    npm create cloudflare@latest -- my-first-worker
    対話プロンプトでは Hello World example → Worker only → 言語は TypeScript を選びます。「Do you want to use git for version control?」は Yes、最後の「Do you want to deploy your application?」は No にします。
  2. cd my-first-worker で移動し、生成物を確認します。wrangler.jsonc(設定ファイル)、src/index.ts(Worker 本体)が中心です。
    {
    	"name": "my-first-worker",
    	"main": "src/index.ts",
    	"compatibility_date": "2026-08-10"
    }
  3. ローカル開発サーバーを起動します。初回は Cloudflare アカウントへのログインがブラウザで求められます。
    npx wrangler dev
    ブラウザで http://localhost:8787 を開くと Hello World! が表示されます。
  4. src/index.ts の返却文字列を "Hello Worker!" に書き換えて保存し、ブラウザをリロードして表示が変わることを確認します。
    export default {
    	async fetch(request, env, ctx): Promise<Response> {
    		return new Response("Hello Worker!");
    	},
    } satisfies ExportedHandler<Env>;
  5. デプロイします。サブドメイン未設定の場合はプロンプトが案内してくれます。
    npx wrangler deploy

出力に表示される https://my-first-worker.<YOUR_SUBDOMAIN>.workers.dev(<YOUR_SUBDOMAIN> はアカウントごとの workers.dev サブドメイン名)へアクセスし、Hello Worker! が世界中どこからでも返ることを確認できれば成功です。

ハンズオン 2-2|URL ルーティングと JSON API

URL API でパスとメソッドによる分岐を実装し、小さな JSON API を作ります。所要 10 分。2-1 のプロジェクトをそのまま使います。

  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>;
  2. 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 で確認するのがポイントです。

ハンズオン 2-3|HTMLRewriter で HTML を書き換え、wrangler tail で観測する

fetch() サブリクエストで外部ページを取得し、HTMLRewriter でストリーミング変換します。デプロイ後は wrangler tail でログを観測します。所要 15 分。

  1. 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>;
  2. npx wrangler deploy でデプロイします。
  3. 別のターミナルでログの購読を開始します。
    npx wrangler tail
  4. ブラウザで workers.dev URL を開きます。Cloudflare docs のページが表示され、ブラウザのタブタイトルだけが書き換わっています。

tail 側のターミナルには、リクエストごとに console.log の内容と実行結果を含む構造化 JSON("outcome": "ok"、logs 配列に upstream status: 200 など)が流れてくれば成功です。npx wrangler tail | jq .event.request.url のように jq と組み合わせた絞り込みもできます。

つまずきポイント

グローバルスコープで fetch() を呼ぶと例外になる

fetch() などの非同期 API は Request コンテキスト(ハンドラの内側)でのみ使えます。モジュールのトップレベルで const promise = fetch("https://example.com/") のように書くとスクリプト起動時に例外が発生し、Worker 全体が動きません。必ず fetch() ハンドラの中から呼びます。

グローバル変数に保存した状態は消える・共有されない

isolate はリソース逼迫などで破棄されることがあり、2 つのリクエストが同じインスタンスに届く保証もありません。「グローバル変数のカウンタが勝手にリセットされる」「値がリクエスト間で見えたり見えなかったりする」のは正常な挙動です。永続化には KV や Durable Objects(後の章)を使います。

初回デプロイ直後の workers.dev で 523 エラーが出る

*.workers.dev サブドメインへの初回デプロイ直後は 523 エラーが表示されることがあります。これは 1 分ほど待つと自然に解消すると公式ドキュメントに明記されています。慌てて再デプロイする必要はありません。

wrangler tail のログが欠ける(sampling mode)

トラフィックが多い 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 の利用が推奨されています。

まとめと次章へ

次章では、この章で素通りした第 2 引数 env の中身 — wrangler.jsonc の設定、環境変数とシークレット、KV や R2 につながるバインディング、Cron Triggers による定期実行 — を扱い、Worker を「本物のアプリケーション」に近づけます。

参考リンク: