Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

Workers のベストプラクティス

最終更新 Markdown で表示Agent セットアップ

本番環境のパターン、Cloudflare 自身の社内利用、開発者コミュニティでよく見られる問題に基づく Workers のベストプラクティスです。

設定

compatibility date を最新に保つ

compatibility_date は、Worker で利用できるランタイム機能とバグ修正を制御します。新規プロジェクトでは今日の日付に設定すると、最新の動作を得られます。既存プロジェクトでも定期的に更新すると、コードを変えずに新しい API と修正を利用できます。

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-09-20",
	"compatibility_flags": ["nodejs_compat"],
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-09-20"
compatibility_flags = [ "nodejs_compat" ]

詳細は Compatibility dates を参照してください。

nodejs_compat を有効にする

nodejs_compat 互換性フラグを付けると、Worker から node:cryptonode:buffernode:stream などの Node.js 組み込みモジュールを使えます。多くのライブラリがこれらのモジュールに依存しています。このフラグを有効にすると、実行時の分かりにくい import エラーを避けられます。

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-09-20",
	"compatibility_flags": ["nodejs_compat"],
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-09-20"
compatibility_flags = [ "nodejs_compat" ]

詳細は Node.js 互換性 を参照してください。

wrangler types でバインディング型を生成する

Env インターフェースは手書きしないでください。wrangler types を実行し、実際の Wrangler 設定と一致する型定義ファイルを生成します。設定とコードの不一致を、デプロイ時ではなくコンパイル時に検出できます。

バインディングを追加または名前変更したら、その都度 wrangler types を再実行します。

npx wrangler types
src/index.jsjs
// ✅ Good: Env is generated by wrangler types and always matches your config
// Do not manually define Env — it drifts from your actual bindings

export default {
	async fetch(request, env) {
		// env.MY_KV, env.MY_BUCKET, etc. are all correctly typed
		const value = await env.MY_KV.get("key");
		return new Response(value);
	},
};
src/index.tsts
// ✅ Good: Env is generated by wrangler types and always matches your config
// Do not manually define Env — it drifts from your actual bindings

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		// env.MY_KV, env.MY_BUCKET, etc. are all correctly typed
		const value = await env.MY_KV.get("key");
		return new Response(value);
	},
} satisfies ExportedHandler<Env>;

詳細は wrangler types を参照してください。

シークレットはソースではなく wrangler secret で保存する

シークレット(API キー、トークン、データベース認証情報)を Wrangler の設定やソースコードに書いてはいけません。wrangler secret put で安全に保存し、実行時は env 経由でアクセスします。ローカル開発では .env ファイルを使い、.gitignore に含めます。詳細は 環境変数 を参照してください。

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-09-20",
	"compatibility_flags": ["nodejs_compat"],

	// ✅ Good: non-secret configuration lives in version control
	"vars": {
		"API_BASE_URL": "https://api.example.com",
	},

	// 🔴 Bad: never put secrets here
	// "API_KEY": "sk-live-abc123..."
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-09-20"
compatibility_flags = [ "nodejs_compat" ]

[vars]
API_BASE_URL = "https://api.example.com"

シークレットを追加するには、次のコマンドを実行し、求められたら対話的にシークレットを入力します。

npx wrangler secret put API_KEY

他のツールや環境変数からシークレットをパイプすることもできます。

# Pipe from another CLI tool
npx some-cli-tool --get-secret | npx wrangler secret put API_KEY
# Pipe from an environment variable or .env file
echo "$API_KEY" | npx wrangler secret put API_KEY

詳細は Secrets を参照してください。

環境は意図して設定する

Wrangler の環境 を使うと、同じコードを本番、ステージング、開発向けの別 Workers にデプロイできます。各環境は {name}-{env} という名前の別 Worker になります(例: my-api-productionmy-api-staging)。

各環境は独立して扱われます。バインディングと vars は環境ごとに宣言する必要があり、継承されません。継承されないキー を参照してください。ルートの Worker(環境サフィックスなし)は別デプロイです。使う予定がなければ、--env で環境を指定せずにデプロイしないでください。

{
	"name": "my-api",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-09-20",
	"compatibility_flags": ["nodejs_compat"],

	// This binding only applies to the root Worker
	"kv_namespaces": [{ "binding": "CACHE", "id": "dev-kv-id" }],

	"env": {
		// Production environment: deploys as "my-api-production"
		"production": {
			"kv_namespaces": [{ "binding": "CACHE", "id": "prod-kv-id" }],
			"routes": [
				{ "pattern": "api.example.com/*", "zone_name": "example.com" },
			],
		},
		// Staging environment: deploys as "my-api-staging"
		"staging": {
			"kv_namespaces": [{ "binding": "CACHE", "id": "staging-kv-id" }],
			"routes": [
				{ "pattern": "api-staging.example.com/*", "zone_name": "example.com" },
			],
		},
	},
}
name = "my-api"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-09-20"
compatibility_flags = [ "nodejs_compat" ]

[[kv_namespaces]]
binding = "CACHE"
id = "dev-kv-id"

[[env.production.kv_namespaces]]
binding = "CACHE"
id = "prod-kv-id"

[[env.production.routes]]
pattern = "api.example.com/*"
zone_name = "example.com"

[[env.staging.kv_namespaces]]
binding = "CACHE"
id = "staging-kv-id"

[[env.staging.routes]]
pattern = "api-staging.example.com/*"
zone_name = "example.com"

この設定ファイルでステージングにデプロイするには、次を実行します。

npx wrangler deploy --env staging

詳細は Environments を参照してください。

カスタムドメインまたはルートを正しく設定する

Workers は 2 つのルーティング方式に対応しており、目的が異なります。

  • カスタムドメイン: Worker 自体がオリジンです。Cloudflare が DNS レコードと SSL 証明書を自動作成します。ホスト名のすべてのトラフィックを Worker が処理する場合に使います。
  • ルート: Worker は既存のオリジンサーバーの手前で実行されます。ルートを追加する前に、そのホスト名に Cloudflare でプロキシされた(オレンジクラウドの)DNS レコードが必要です。

ルートで最も多い失敗は、DNS レコードの欠落です。プロキシされた DNS レコードがないと、そのホスト名へのリクエストは ERR_NAME_NOT_RESOLVED を返し、Worker に到達しません。実際のオリジンがない場合は、プレースホルダーとして 100:: を指すプロキシ済みの AAAA レコードを追加します。

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-09-20",
	"compatibility_flags": ["nodejs_compat"],

	// Option 1: Custom domain — Worker is the origin, DNS is managed automatically
	"routes": [{ "pattern": "api.example.com", "custom_domain": true }],

	// Option 2: Route — Worker runs in front of an existing origin
	// Requires a proxied DNS record for shop.example.com
	// "routes": [
	// 	{ "pattern": "shop.example.com/*", "zone_name": "example.com" }
	// ]
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-09-20"
compatibility_flags = [ "nodejs_compat" ]

[[routes]]
pattern = "api.example.com"
custom_domain = true

詳細は Routing を参照してください。

リクエストとレスポンスの処理

リクエストとレスポンスのボディをストリーミングする

メモリ上限に関係なく、大きなリクエストとレスポンスをストリーミングするのは、どの言語でもベストプラクティスです。ピーク時のメモリ使用量を下げ、最初のバイトまでの時間を短縮できます。Workers の メモリ上限は 128 MB です。await response.text()await request.arrayBuffer() でボディ全体をバッファすると、大きなペイロードで Worker がクラッシュします。

JSON ペイロードやファイルアップロードなど、ボディをすべて消費する場合は、読む前に最大サイズを制限します。処理したくないデータをクライアントが送るのを防げます。

TransformStream を使い、ソースから宛先へデータをパイプして、Worker 内でメモリにすべて保持せずにストリーミングします。

src/index.jsjs
// 🔴 Bad: buffers the entire response body in memory
const badHandler = {
	async fetch(request, env) {
		const response = await fetch("https://api.example.com/large-dataset");
		const text = await response.text();
		return new Response(text);
	},
};

// ✅ Good: stream the response body through without buffering
export default {
	async fetch(request, env) {
		const response = await fetch("https://api.example.com/large-dataset");
		return new Response(response.body, response);
	},
};
src/index.tsts
// 🔴 Bad: buffers the entire response body in memory
const badHandler = {
	async fetch(request: Request, env: Env): Promise<Response> {
		const response = await fetch("https://api.example.com/large-dataset");
		const text = await response.text();
		return new Response(text);
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: stream the response body through without buffering
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const response = await fetch("https://api.example.com/large-dataset");
		return new Response(response.body, response);
	},
} satisfies ExportedHandler<Env>;

複数のレスポンスを連結する必要がある場合(複数の上流 API からデータを取得する場合など)は、各ボディを 1 つの書き込み可能なストリームへ順にパイプします。どのレスポンスもメモリにバッファせずに済みます。

src/concat.jsjs
export default {
	async fetch(request, env) {
		const urls = [
			"https://api.example.com/part-1",
			"https://api.example.com/part-2",
			"https://api.example.com/part-3",
		];

		const { readable, writable } = new TransformStream();

		// ✅ Good: pipe each response body sequentially without buffering
		const pipeline = (async () => {
			for (const url of urls) {
				const response = await fetch(url);
				if (response.body) {
					// pipeTo with preventClose keeps the writable open for the next response
					await response.body.pipeTo(writable, {
						preventClose: true,
					});
				}
			}
			await writable.close();
		})();

		// Return the readable side immediately — data streams as it arrives
		return new Response(readable, {
			headers: { "Content-Type": "application/octet-stream" },
		});
	},
};
src/concat.tsts
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const urls = [
			"https://api.example.com/part-1",
			"https://api.example.com/part-2",
			"https://api.example.com/part-3",
		];

		const { readable, writable } = new TransformStream();

		// ✅ Good: pipe each response body sequentially without buffering
		const pipeline = (async () => {
			for (const url of urls) {
				const response = await fetch(url);
				if (response.body) {
					// pipeTo with preventClose keeps the writable open for the next response
					await response.body.pipeTo(writable, {
						preventClose: true,
					});
				}
			}
			await writable.close();
		})();

		// Return the readable side immediately — data streams as it arrives
		return new Response(readable, {
			headers: { "Content-Type": "application/octet-stream" },
		});
	},
} satisfies ExportedHandler<Env>;

詳細は Streams を参照してください。

レスポンス後の処理には waitUntil を使う

ctx.waitUntil() を使うと、レスポンスをクライアントへ送ったあとに処理できます。分析、キャッシュ書き込み、ログ、Webhook 通知などが該当します。レスポンスを速く保ったまま、バックグラウンド処理を完了できます。

ctx.waitUntil() は、レスポンスに影響しない処理にだけ使います。レスポンスがその処理に依存する場合は、返す前に await するか、処理の完了に合わせてレスポンスをストリーミングします。レスポンスボディをストリーミング中の Worker は、ctx.waitUntil() なしでもアクティブなままです。

よくある失敗は 2 つです。ctx を分割代入すると this バインディングが失われ、「Illegal invocation」になります。また、レスポンス送信後またはクライアント切断後の waitUntil() 制限時間は 30 秒です。

src/index.jsjs
// 🔴 Bad: destructuring ctx loses the `this` binding
const badHandler = {
	async fetch(request, env, ctx) {
		const { waitUntil } = ctx; // "Illegal invocation" at runtime
		waitUntil(fetch("https://analytics.example.com/events"));
		return new Response("OK");
	},
};

// ✅ Good: send the response immediately, do background work after
export default {
	async fetch(request, env, ctx) {
		const data = await processRequest(request);

		ctx.waitUntil(logToAnalytics(env, data));
		ctx.waitUntil(updateCache(env, data));

		return Response.json(data);
	},
};

async function logToAnalytics(env, data) {
	await fetch("https://analytics.example.com/events", {
		method: "POST",
		body: JSON.stringify(data),
	});
}

async function updateCache(env, data) {
	await env.CACHE.put("latest", JSON.stringify(data));
}
src/index.tsts
// 🔴 Bad: destructuring ctx loses the `this` binding
const badHandler = {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		const { waitUntil } = ctx; // "Illegal invocation" at runtime
		waitUntil(fetch("https://analytics.example.com/events"));
		return new Response("OK");
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: send the response immediately, do background work after
export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		const data = await processRequest(request);

		ctx.waitUntil(logToAnalytics(env, data));
		ctx.waitUntil(updateCache(env, data));

		return Response.json(data);
	},
} satisfies ExportedHandler<Env>;

async function logToAnalytics(env: Env, data: unknown): Promise<void> {
	await fetch("https://analytics.example.com/events", {
		method: "POST",
		body: JSON.stringify(data),
	});
}

async function updateCache(env: Env, data: unknown): Promise<void> {
	await env.CACHE.put("latest", JSON.stringify(data));
}

詳細は Context を参照してください。

アーキテクチャ

Cloudflare サービスには REST API ではなくバインディングを使う

R2、KV、D1、Queues、Workflows などの一部の Cloudflare サービスは バインディング として利用できます。バインディングはプロセス内の直接参照であり、ネットワークホップ、認証、追加のレイテンシは不要です。Worker 内から REST API を呼ぶと時間が無駄になり、不要な複雑さが増えます。

src/index.jsjs
// 🔴 Bad: calling the REST API from a Worker
const badHandler = {
	async fetch(request, env) {
		const response = await fetch(
			"https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/r2/buckets/BUCKET_NAME/objects/my-file",
			{ headers: { Authorization: `Bearer ${env.CF_API_TOKEN}` } },
		);
		return new Response(response.body);
	},
};

// ✅ Good: use the binding directly — no network hop, no auth needed
export default {
	async fetch(request, env) {
		const object = await env.MY_BUCKET.get("my-file");

		if (!object) {
			return new Response("Not found", { status: 404 });
		}

		return new Response(object.body, {
			headers: {
				"Content-Type":
					object.httpMetadata?.contentType ?? "application/octet-stream",
			},
		});
	},
};
src/index.tsts
// 🔴 Bad: calling the REST API from a Worker
const badHandler = {
	async fetch(request: Request, env: Env): Promise<Response> {
		const response = await fetch(
			"https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/r2/buckets/BUCKET_NAME/objects/my-file",
			{ headers: { Authorization: `Bearer ${env.CF_API_TOKEN}` } },
		);
		return new Response(response.body);
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: use the binding directly — no network hop, no auth needed
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const object = await env.MY_BUCKET.get("my-file");

		if (!object) {
			return new Response("Not found", { status: 404 });
		}

		return new Response(object.body, {
			headers: {
				"Content-Type":
					object.httpMetadata?.contentType ?? "application/octet-stream",
			},
		});
	},
} satisfies ExportedHandler<Env>;

非同期・バックグラウンド処理には Queues と Workflows を使う

長時間、再試行が必要、または緊急性の低い処理でリクエストをブロックしてはいけません。QueuesWorkflows を使い、処理をクリティカルパスから外します。目的は次のように異なります。

Queues を使う場合 は、プロデューサーとコンシューマーを切り離したいときです。Queues はメッセージブローカーです。ある Worker がメッセージを送り、別の Worker が後で処理します。ファンアウト(1 つのイベントが多数のコンシューマーを起動する)、バッファとバッチ(下流サービスへ書く前にメッセージを集約する)、単一ステップの簡単なバックグラウンドジョブ(メール送信、Webhook 発火、ログ書き込み)に向いています。Queues はメッセージごとに設定可能な再試行付きで、少なくとも 1 回の配信を提供します。

Workflows を使う場合 は、バックグラウンド処理に相互依存する複数ステップがあるときです。Workflows は耐久実行エンジンです。各ステップの戻り値は永続化され、ステップが失敗してもジョブ全体ではなく、そのステップだけが再試行されます。複数ステップの処理(カード決済、出荷作成、確認送信)、一時停止と再開が必要な長時間タスク(step.waitForEvent() で外部イベントや人の承認を数時間〜数日待つ)、後続ステップが先行結果に依存する複雑な条件分岐に向いています。Workflows は数時間、数日、数週間実行できます。

両方を組み合わせる場合 は、高スループットの入口が複雑な処理につながるときです。たとえば Queue で受信注文をバッファし、コンシューマーが複数ステップのフルフィルメントが必要な注文ごとに Workflow インスタンスを作成できます。

src/index.jsjs
export default {
	async fetch(request, env) {
		const order = await request.json();

		if (order.type === "simple") {
			// ✅ Queue: single-step background job — send a message for async processing
			await env.ORDER_QUEUE.send({
				orderId: order.id,
				action: "send-confirmation-email",
			});
		} else {
			// ✅ Workflow: multi-step durable process — payment, fulfillment, notification
			const instance = await env.FULFILLMENT_WORKFLOW.create({
				params: { orderId: order.id },
			});
		}

		return Response.json({ status: "accepted" }, { status: 202 });
	},
};
src/index.tsts
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const order = await request.json<{ id: string; type: string }>();

		if (order.type === "simple") {
			// ✅ Queue: single-step background job — send a message for async processing
			await env.ORDER_QUEUE.send({
				orderId: order.id,
				action: "send-confirmation-email",
			});
		} else {
			// ✅ Workflow: multi-step durable process — payment, fulfillment, notification
			const instance = await env.FULFILLMENT_WORKFLOW.create({
				params: { orderId: order.id },
			});
		}

		return Response.json({ status: "accepted" }, { status: 202 });
	},
} satisfies ExportedHandler<Env>;

詳細は QueuesWorkflows を参照してください。

Worker 間通信にはサービスバインディングを使う

ある Worker が別の Worker を呼ぶ必要があるときは、公開 URL へ HTTP リクエストするのではなく サービスバインディング を使います。サービスバインディングは追加コストがなく、パブリックインターネットを経由せず、型安全な RPC に対応しています。

src/index.jsjs
import { WorkerEntrypoint } from "cloudflare:workers";

// The "auth" Worker exposes RPC methods
export class AuthService extends WorkerEntrypoint {
	async verifyToken(token) {
		// Token verification logic
		return { userId: "user-123", valid: true };
	}
}

// The "api" Worker calls the auth Worker via a service binding
export default {
	async fetch(request, env) {
		const token = request.headers.get("Authorization")?.replace("Bearer ", "");

		if (!token) {
			return new Response("Unauthorized", { status: 401 });
		}

		// ✅ Good: call another Worker via service binding RPC — no network hop
		const auth = await env.AUTH_SERVICE.verifyToken(token);

		if (!auth.valid) {
			return new Response("Invalid token", { status: 403 });
		}

		return Response.json({ userId: auth.userId });
	},
};
src/index.tsts
import { WorkerEntrypoint } from "cloudflare:workers";

// The "auth" Worker exposes RPC methods
export class AuthService extends WorkerEntrypoint {
	async verifyToken(
		token: string,
	): Promise<{ userId: string; valid: boolean }> {
		// Token verification logic
		return { userId: "user-123", valid: true };
	}
}

// The "api" Worker calls the auth Worker via a service binding
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const token = request.headers.get("Authorization")?.replace("Bearer ", "");

		if (!token) {
			return new Response("Unauthorized", { status: 401 });
		}

		// ✅ Good: call another Worker via service binding RPC — no network hop
		const auth = await env.AUTH_SERVICE.verifyToken(token);

		if (!auth.valid) {
			return new Response("Invalid token", { status: 403 });
		}

		return Response.json({ userId: auth.userId });
	},
} satisfies ExportedHandler<Env>;

外部データベース接続には Hyperdrive を使う

Worker からリモートの PostgreSQL または MySQL データベースへ接続するときは、必ず Hyperdrive を使います。Hyperdrive はデータベースの近くにリージョンの接続プールを維持し、リクエストごとの TCP ハンドシェイク、TLS ネゴシエーション、接続セットアップのコストをなくします。可能な場合はクエリ結果もキャッシュします。

リクエストごとに新しい Client を作成します。背後のプールは Hyperdrive が管理するため、クライアント作成は高速です。データベースドライバーのサポートには nodejs_compat が必要です。

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-09-20",
	"compatibility_flags": ["nodejs_compat"],

	"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "<YOUR_HYPERDRIVE_ID>" }],
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-09-20"
compatibility_flags = [ "nodejs_compat" ]

[[hyperdrive]]
binding = "HYPERDRIVE"
id = "<YOUR_HYPERDRIVE_ID>"
src/index.jsjs
import { Client } from "pg";

export default {
	async fetch(request, env) {
		// ✅ Good: create a new client per request — Hyperdrive pools the underlying connection
		const client = new Client({
			connectionString: env.HYPERDRIVE.connectionString,
		});

		try {
			await client.connect();
			const result = await client.query("SELECT id, name FROM users LIMIT 10");
			return Response.json(result.rows);
		} catch (e) {
			console.error(
				JSON.stringify({ message: "database query failed", error: String(e) }),
			);
			return Response.json({ error: "Database error" }, { status: 500 });
		}
	},
};

// 🔴 Bad: connecting directly to a remote database without Hyperdrive
// Every request pays the full TCP + TLS + auth cost (often 300-500ms)
const badHandler = {
	async fetch(request, env) {
		const client = new Client({
			connectionString: "postgres://user:pass@db.example.com:5432/mydb",
		});
		await client.connect();
		const result = await client.query("SELECT id, name FROM users LIMIT 10");
		return Response.json(result.rows);
	},
};
src/index.tsts
import { Client } from "pg";

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		// ✅ Good: create a new client per request — Hyperdrive pools the underlying connection
		const client = new Client({
			connectionString: env.HYPERDRIVE.connectionString,
		});

		try {
			await client.connect();
			const result = await client.query("SELECT id, name FROM users LIMIT 10");
			return Response.json(result.rows);
		} catch (e) {
			console.error(
				JSON.stringify({ message: "database query failed", error: String(e) }),
			);
			return Response.json({ error: "Database error" }, { status: 500 });
		}
	},
} satisfies ExportedHandler<Env>;

// 🔴 Bad: connecting directly to a remote database without Hyperdrive
// Every request pays the full TCP + TLS + auth cost (often 300-500ms)
const badHandler = {
	async fetch(request: Request, env: Env): Promise<Response> {
		const client = new Client({
			connectionString: "postgres://user:pass@db.example.com:5432/mydb",
		});
		await client.connect();
		const result = await client.query("SELECT id, name FROM users LIMIT 10");
		return Response.json(result.rows);
	},
} satisfies ExportedHandler<Env>;

詳細は Hyperdrive を参照してください。

WebSocket には Durable Objects を使う

通常の Workers でも HTTP 接続を WebSocket にアップグレードできますが、永続状態とハイバネーションはありません。isolate が退去されると、接続を保持する永続アクターがないため、接続は失われます。信頼できる長寿命の WebSocket 接続には、Hibernation API 付きの Durable Objects を使います。Durable Objects は、オブジェクトがメモリから退去されても WebSocket 接続を開き続け、メッセージ到着時に自動で復帰します。

ハイバネーションを有効にするには、ws.accept() ではなく this.ctx.acceptWebSocket() を使います。オブジェクトを起こさない ping/pong ハートビートには setWebSocketAutoResponse を使います。

src/index.jsjs
import { DurableObject } from "cloudflare:workers";

// Parent Worker: upgrades HTTP to WebSocket and routes to a Durable Object
export default {
	async fetch(request, env) {
		if (request.headers.get("Upgrade") !== "websocket") {
			return new Response("Expected WebSocket", { status: 426 });
		}

		const stub = env.CHAT_ROOM.getByName("default-room");
		return stub.fetch(request);
	},
};

// Durable Object: manages WebSocket connections with hibernation
export class ChatRoom extends DurableObject {
	constructor(ctx, env) {
		super(ctx, env);
		// Auto ping/pong without waking the object
		this.ctx.setWebSocketAutoResponse(
			new WebSocketRequestResponsePair("ping", "pong"),
		);
	}

	async fetch(request) {
		const pair = new WebSocketPair();
		const [client, server] = Object.values(pair);

		// ✅ Good: acceptWebSocket enables hibernation
		this.ctx.acceptWebSocket(server);

		return new Response(null, { status: 101, webSocket: client });
	}

	// Called when a message arrives — the object wakes from hibernation if needed
	async webSocketMessage(ws, message) {
		for (const conn of this.ctx.getWebSockets()) {
			conn.send(typeof message === "string" ? message : "binary");
		}
	}

	async webSocketClose(ws, code, reason, wasClean) {
		// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
		// auto-replies to Close frames. Calling close() is safe but no longer required.
		ws.close(code, reason);
	}
}
src/index.tsts
import { DurableObject } from "cloudflare:workers";

// Parent Worker: upgrades HTTP to WebSocket and routes to a Durable Object
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		if (request.headers.get("Upgrade") !== "websocket") {
			return new Response("Expected WebSocket", { status: 426 });
		}

		const stub = env.CHAT_ROOM.getByName("default-room");
		return stub.fetch(request);
	},
} satisfies ExportedHandler<Env>;

// Durable Object: manages WebSocket connections with hibernation
export class ChatRoom extends DurableObject {
	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);
		// Auto ping/pong without waking the object
		this.ctx.setWebSocketAutoResponse(
			new WebSocketRequestResponsePair("ping", "pong"),
		);
	}

	async fetch(request: Request): Promise<Response> {
		const pair = new WebSocketPair();
		const [client, server] = Object.values(pair);

		// ✅ Good: acceptWebSocket enables hibernation
		this.ctx.acceptWebSocket(server);

		return new Response(null, { status: 101, webSocket: client });
	}

	// Called when a message arrives — the object wakes from hibernation if needed
	async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
		for (const conn of this.ctx.getWebSockets()) {
			conn.send(typeof message === "string" ? message : "binary");
		}
	}

	async webSocketClose(
		ws: WebSocket,
		code: number,
		reason: string,
		wasClean: boolean,
	) {
		// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
		// auto-replies to Close frames. Calling close() is safe but no longer required.
		ws.close(code, reason);
	}
}

詳細は Durable Objects の WebSocket ベストプラクティス を参照してください。

新規プロジェクトには Workers Static Assets を使う

Workers Static Assets は、静的サイト、シングルページアプリケーション、フルスタックアプリを Cloudflare にデプロイする推奨方法です。新規プロジェクトを始める場合は、Pages ではなく Workers を使います。Pages は引き続き動作しますが、新機能と最適化は Workers に集中しています。

純粋な静的サイトでは、ビルド出力を assets.directory に指定します。Worker スクリプトは不要です。フルスタックアプリでは、main エントリポイントと ASSETS バインディングを追加し、API と並べて静的ファイルを配信します。

{
	// Static site — no Worker script needed
	"name": "my-static-site",
	// Set this to today's date
	"compatibility_date": "2026-09-20",
	"compatibility_flags": ["nodejs_compat"],

	"assets": {
		"directory": "./dist",
	},
}
name = "my-static-site"
# Set this to today's date
compatibility_date = "2026-09-20"
compatibility_flags = [ "nodejs_compat" ]

[assets]
directory = "./dist"

詳細は Workers Static Assets を参照してください。

オブザーバビリティ

Workers Logs と Traces を有効にする

オブザーバビリティのない本番 Workers はブラックボックスです。本番へデプロイする前に、ログとトレースを有効にします。間欠的なエラーが出たときに、すでに収集されているデータが診断に必要です。

Wrangler の設定で有効にし、head_sampling_rate で量を制御してコストを管理します。サンプリングレート 1 はすべてを捕捉します。トラフィックが多い Workers では下げます。

検索とフィルターができるよう、console.log で構造化 JSON ログを使います。エラーには console.error、警告には console.warn を使います。Workers Observability ダッシュボードでは、正しい重大度で表示されます。

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-09-20",
	"compatibility_flags": ["nodejs_compat"],

	"observability": {
		"enabled": true,
		"logs": {
			// Capture 100% of logs — lower this for high-traffic Workers
			"head_sampling_rate": 1,
		},
		"traces": {
			"enabled": true,
			"head_sampling_rate": 0.01, // Sample 1% of traces
		},
	},
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-09-20"
compatibility_flags = [ "nodejs_compat" ]

[observability]
enabled = true

  [observability.logs]
  head_sampling_rate = 1

  [observability.traces]
  enabled = true
  head_sampling_rate = 0.01
src/index.jsjs
export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		try {
			// ✅ Good: structured JSON — searchable and filterable in the dashboard
			console.log(
				JSON.stringify({
					message: "incoming request",
					method: request.method,
					path: url.pathname,
				}),
			);

			const result = await env.MY_KV.get(url.pathname);
			return new Response(result ?? "Not found", {
				status: result ? 200 : 404,
			});
		} catch (e) {
			// ✅ Good: console.error appears as "error" severity in Workers Observability
			console.error(
				JSON.stringify({
					message: "request failed",
					error: e instanceof Error ? e.message : String(e),
					path: url.pathname,
				}),
			);
			return Response.json({ error: "Internal server error" }, { status: 500 });
		}
	},
};

// 🔴 Bad: unstructured string logs are hard to query
const badHandler = {
	async fetch(request, env) {
		const url = new URL(request.url);
		console.log("Got a request to " + url.pathname);
		return new Response("OK");
	},
};
src/index.tsts
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		try {
			// ✅ Good: structured JSON — searchable and filterable in the dashboard
			console.log(
				JSON.stringify({
					message: "incoming request",
					method: request.method,
					path: url.pathname,
				}),
			);

			const result = await env.MY_KV.get(url.pathname);
			return new Response(result ?? "Not found", {
				status: result ? 200 : 404,
			});
		} catch (e) {
			// ✅ Good: console.error appears as "error" severity in Workers Observability
			console.error(
				JSON.stringify({
					message: "request failed",
					error: e instanceof Error ? e.message : String(e),
					path: url.pathname,
				}),
			);
			return Response.json({ error: "Internal server error" }, { status: 500 });
		}
	},
} satisfies ExportedHandler<Env>;

// 🔴 Bad: unstructured string logs are hard to query
const badHandler = {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);
		console.log("Got a request to " + url.pathname);
		return new Response("OK");
	},
} satisfies ExportedHandler<Env>;

詳細は Workers LogsTraces を参照してください。

利用できるオブザーバビリティツールの全体像は Workers Observability を参照してください。

コードパターン

リクエストスコープの状態をグローバルスコープに置かない

Workers はリクエスト間で isolate を再利用します。あるリクエストで設定した変数は、次のリクエストでも残っています。リクエスト間のデータ漏洩、古い状態、「Cannot perform I/O on behalf of a different request」エラーの原因になります。

状態は関数の引数で渡すか、env バインディングに保存します。モジュールレベルの変数には置かないでください。

src/index.jsjs
// 🔴 Bad: global mutable state leaks between requests
let currentUser = null;

const badHandler = {
	async fetch(request, env, ctx) {
		// Storing request-scoped data globally means the next request sees stale data
		currentUser = request.headers.get("X-User-Id");
		const result = await handleRequest(currentUser, env);
		return Response.json(result);
	},
};

// ✅ Good: pass request-scoped data through function arguments
export default {
	async fetch(request, env, ctx) {
		const userId = request.headers.get("X-User-Id");
		const result = await handleRequest(userId, env);

		return Response.json(result);
	},
};

async function handleRequest(userId, env) {
	return { userId };
}
src/index.tsts
// 🔴 Bad: global mutable state leaks between requests
let currentUser: string | null = null;

const badHandler = {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		// Storing request-scoped data globally means the next request sees stale data
		currentUser = request.headers.get("X-User-Id");
		const result = await handleRequest(currentUser, env);
		return Response.json(result);
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: pass request-scoped data through function arguments
export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		const userId = request.headers.get("X-User-Id");
		const result = await handleRequest(userId, env);

		return Response.json(result);
	},
} satisfies ExportedHandler<Env>;

async function handleRequest(userId: string | null, env: Env): Promise<object> {
	return { userId };
}

詳細は Workers のエラー を参照してください。

Promise は必ず await するか waitUntil する

awaitreturn、または ctx.waitUntil() に渡していない Promise は floating Promise です。floating Promise は静かなバグの原因になります。結果の欠落、握りつぶされたエラー、未完了の処理です。Workers ランタイムは、floating Promise が完了する前に isolate を終了することがあります。

レスポンスがその処理に依存するかどうかで選びます。レスポンスが正しくなる前に完了しなければならない処理には await または return を使います。レスポンス送信後に実行でき、waitUntil() の制限時間内に終わる処理には ctx.waitUntil() を使います。

開発時に検出するには、no-floating-promises lint ルールを有効にします。ESLint を使う場合は @typescript-eslint/no-floating-promises を有効にします。oxlint を使う場合は typescript/no-floating-promises を有効にします。

# ESLint (typescript-eslint)
npx eslint --rule '{"@typescript-eslint/no-floating-promises": "error"}' src/

# oxlint
npx oxlint --deny typescript/no-floating-promises src/
src/index.jsjs
export default {
	async fetch(request, env, ctx) {
		const data = await request.json();

		// 🔴 Bad: floating promise — result is dropped, errors are swallowed
		fetch("https://api.example.com/webhook", {
			method: "POST",
			body: JSON.stringify(data),
		});

		// ✅ Good: await if you need the result before responding
		const response = await fetch("https://api.example.com/process", {
			method: "POST",
			body: JSON.stringify(data),
		});

		// ✅ Good: waitUntil if you do not need the result before responding
		ctx.waitUntil(
			fetch("https://api.example.com/webhook", {
				method: "POST",
				body: JSON.stringify(data),
			}),
		);

		return new Response("OK");
	},
};
src/index.tsts
export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		const data = await request.json();

		// 🔴 Bad: floating promise — result is dropped, errors are swallowed
		fetch("https://api.example.com/webhook", {
			method: "POST",
			body: JSON.stringify(data),
		});

		// ✅ Good: await if you need the result before responding
		const response = await fetch("https://api.example.com/process", {
			method: "POST",
			body: JSON.stringify(data),
		});

		// ✅ Good: waitUntil if you do not need the result before responding
		ctx.waitUntil(
			fetch("https://api.example.com/webhook", {
				method: "POST",
				body: JSON.stringify(data),
			}),
		);

		return new Response("OK");
	},
} satisfies ExportedHandler<Env>;

セキュリティ

安全なトークン生成には Web Crypto を使う

Workers ランタイムは、暗号処理向けに Web Crypto API を提供します。一意の識別子には crypto.randomUUID()、乱数バイトには crypto.getRandomValues() を使います。セキュリティに関わる用途で Math.random() を使ってはいけません。暗号学的に安全ではありません。

nodejs_compat が有効な場合、Node.js の node:crypto も完全にサポートされます。自分やライブラリが好みの API を使えます。

src/index.jsjs
export default {
	async fetch(request, env) {
		// 🔴 Bad: Math.random() is predictable and not suitable for security
		const badToken = Math.random().toString(36).substring(2);

		// ✅ Good: cryptographically secure random UUID
		const sessionId = crypto.randomUUID();

		// ✅ Good: cryptographically secure random bytes for tokens
		const tokenBytes = new Uint8Array(32);
		crypto.getRandomValues(tokenBytes);
		const token = Array.from(tokenBytes)
			.map((b) => b.toString(16).padStart(2, "0"))
			.join("");

		return Response.json({ sessionId, token });
	},
};
src/index.tsts
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		// 🔴 Bad: Math.random() is predictable and not suitable for security
		const badToken = Math.random().toString(36).substring(2);

		// ✅ Good: cryptographically secure random UUID
		const sessionId = crypto.randomUUID();

		// ✅ Good: cryptographically secure random bytes for tokens
		const tokenBytes = new Uint8Array(32);
		crypto.getRandomValues(tokenBytes);
		const token = Array.from(tokenBytes)
			.map((b) => b.toString(16).padStart(2, "0"))
			.join("");

		return Response.json({ sessionId, token });
	},
} satisfies ExportedHandler<Env>;

シークレット値(API キー、トークン、HMAC 署名)を比較するときは、タイミングサイドチャネル攻撃を防ぐために crypto.subtle.timingSafeEqual() を使います。長さの不一致で短絡評価してはいけません。先に両方の値を固定サイズのハッシュにエンコードします。

src/verify.jsjs
async function verifyToken(provided, expected) {
	const encoder = new TextEncoder();

	// ✅ Good: hash both values to a fixed size, then compare in constant time
	// This avoids leaking the length of the expected value
	const [providedHash, expectedHash] = await Promise.all([
		crypto.subtle.digest("SHA-256", encoder.encode(provided)),
		crypto.subtle.digest("SHA-256", encoder.encode(expected)),
	]);

	return crypto.subtle.timingSafeEqual(providedHash, expectedHash);
}

// 🔴 Bad: direct string comparison leaks timing information
function verifyTokenInsecure(provided, expected) {
	return provided === expected;
}
src/verify.tsts
async function verifyToken(
	provided: string,
	expected: string,
): Promise<boolean> {
	const encoder = new TextEncoder();

	// ✅ Good: hash both values to a fixed size, then compare in constant time
	// This avoids leaking the length of the expected value
	const [providedHash, expectedHash] = await Promise.all([
		crypto.subtle.digest("SHA-256", encoder.encode(provided)),
		crypto.subtle.digest("SHA-256", encoder.encode(expected)),
	]);

	return crypto.subtle.timingSafeEqual(providedHash, expectedHash);
}

// 🔴 Bad: direct string comparison leaks timing information
function verifyTokenInsecure(provided: string, expected: string): boolean {
	return provided === expected;
}

エラー処理に passThroughOnException を使わない

passThroughOnException() は、Worker が未処理例外を投げたときにリクエストをオリジンへ送る fail-open の仕組みです。オリジンサーバーからの移行中には役立つことがありますが、バグを隠し、デバッグを難しくします。代わりに、明示的な try...catch と構造化されたエラーレスポンスを使います。

src/index.jsjs
// 🔴 Bad: hides errors by falling through to origin
const badHandler = {
	async fetch(request, env, ctx) {
		ctx.passThroughOnException();
		const result = await handleRequest(request, env);
		return Response.json(result);
	},
};

// ✅ Good: explicit error handling with structured responses
export default {
	async fetch(request, env, ctx) {
		try {
			const result = await handleRequest(request, env);
			return Response.json(result);
		} catch (error) {
			const message = error instanceof Error ? error.message : "Unknown error";

			console.error(
				JSON.stringify({
					message: "unhandled error",
					error: message,
					path: new URL(request.url).pathname,
				}),
			);

			return Response.json({ error: "Internal server error" }, { status: 500 });
		}
	},
};

async function handleRequest(request, env) {
	return { status: "ok" };
}
src/index.tsts
// 🔴 Bad: hides errors by falling through to origin
const badHandler = {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		ctx.passThroughOnException();
		const result = await handleRequest(request, env);
		return Response.json(result);
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: explicit error handling with structured responses
export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		try {
			const result = await handleRequest(request, env);
			return Response.json(result);
		} catch (error) {
			const message = error instanceof Error ? error.message : "Unknown error";

			console.error(
				JSON.stringify({
					message: "unhandled error",
					error: message,
					path: new URL(request.url).pathname,
				}),
			);

			return Response.json({ error: "Internal server error" }, { status: 500 });
		}
	},
} satisfies ExportedHandler<Env>;

async function handleRequest(request: Request, env: Env): Promise<object> {
	return { status: "ok" };
}

開発とテスト

@cloudflare/vitest-plugin でテストする

@cloudflare/vitest-plugin パッケージは、Workers ランタイム内でテストを実行します。テスト中に実際のバインディング(KV、R2、D1、Durable Objects)へアクセスできます。未対応 API や不足している互換性フラグなど、Node.js ベースのテストが見逃す問題を検出できます。

既知の落とし穴が 1 つあります。Vitest プラグインは nodejs_compat を自動注入するため、Wrangler 設定にフラグがなくてもテストは通ります。コードが Node.js 組み込みモジュールに依存する場合は、wrangler.jsoncnodejs_compat があることを必ず確認してください。

test/index.test.jsjs
import { describe, it, expect } from "vitest";
import { env } from "cloudflare:workers";

describe("KV operations", () => {
	it("should store and retrieve a value", async () => {
		await env.MY_KV.put("key", "value");
		const result = await env.MY_KV.get("key");
		expect(result).toBe("value");
	});

	it("should return null for missing keys", async () => {
		const result = await env.MY_KV.get("nonexistent");
		// ✅ Good: test the null case explicitly
		expect(result).toBeNull();
	});
});
test/index.test.tsts
import { describe, it, expect } from "vitest";
import { env } from "cloudflare:workers";

describe("KV operations", () => {
	it("should store and retrieve a value", async () => {
		await env.MY_KV.put("key", "value");
		const result = await env.MY_KV.get("key");
		expect(result).toBe("value");
	});

	it("should return null for missing keys", async () => {
		const result = await env.MY_KV.get("nonexistent");
		// ✅ Good: test the null case explicitly
		expect(result).toBeNull();
	});
});

詳細は Vitest でのテスト を参照してください。

関連リソース

役に立ちましたか?