本番環境のパターン、Cloudflare 自身の社内利用、開発者コミュニティでよく見られる問題に基づく Workers のベストプラクティスです。
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 互換性フラグを付けると、Worker から node:crypto、node:buffer、node: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 互換性 を参照してください。
Env インターフェースは手書きしないでください。wrangler types を実行し、実際の Wrangler 設定と一致する型定義ファイルを生成します。設定とコードの不一致を、デプロイ時ではなくコンパイル時に検出できます。
バインディングを追加または名前変更したら、その都度 wrangler types を再実行します。
npx wrangler typesyarn wrangler typespnpm wrangler types// ✅ 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);
},
};// ✅ 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 を参照してください。
シークレット(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_KEYyarn wrangler secret put API_KEYpnpm 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-production と my-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 stagingyarn wrangler deploy --env stagingpnpm 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 内でメモリにすべて保持せずにストリーミングします。
// 🔴 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);
},
};// 🔴 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 つの書き込み可能なストリームへ順にパイプします。どのレスポンスもメモリにバッファせずに済みます。
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" },
});
},
};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 を参照してください。
ctx.waitUntil() を使うと、レスポンスをクライアントへ送ったあとに処理できます。分析、キャッシュ書き込み、ログ、Webhook 通知などが該当します。レスポンスを速く保ったまま、バックグラウンド処理を完了できます。
ctx.waitUntil() は、レスポンスに影響しない処理にだけ使います。レスポンスがその処理に依存する場合は、返す前に await するか、処理の完了に合わせてレスポンスをストリーミングします。レスポンスボディをストリーミング中の Worker は、ctx.waitUntil() なしでもアクティブなままです。
よくある失敗は 2 つです。ctx を分割代入すると this バインディングが失われ、「Illegal invocation」になります。また、レスポンス送信後またはクライアント切断後の waitUntil() 制限時間は 30 秒です。
// 🔴 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));
}// 🔴 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 を参照してください。
R2、KV、D1、Queues、Workflows などの一部の Cloudflare サービスは バインディング として利用できます。バインディングはプロセス内の直接参照であり、ネットワークホップ、認証、追加のレイテンシは不要です。Worker 内から REST API を呼ぶと時間が無駄になり、不要な複雑さが増えます。
// 🔴 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",
},
});
},
};// 🔴 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 を使い、処理をクリティカルパスから外します。目的は次のように異なります。
Queues を使う場合 は、プロデューサーとコンシューマーを切り離したいときです。Queues はメッセージブローカーです。ある Worker がメッセージを送り、別の Worker が後で処理します。ファンアウト(1 つのイベントが多数のコンシューマーを起動する)、バッファとバッチ(下流サービスへ書く前にメッセージを集約する)、単一ステップの簡単なバックグラウンドジョブ(メール送信、Webhook 発火、ログ書き込み)に向いています。Queues はメッセージごとに設定可能な再試行付きで、少なくとも 1 回の配信を提供します。
Workflows を使う場合 は、バックグラウンド処理に相互依存する複数ステップがあるときです。Workflows は耐久実行エンジンです。各ステップの戻り値は永続化され、ステップが失敗してもジョブ全体ではなく、そのステップだけが再試行されます。複数ステップの処理(カード決済、出荷作成、確認送信)、一時停止と再開が必要な長時間タスク(step.waitForEvent() で外部イベントや人の承認を数時間〜数日待つ)、後続ステップが先行結果に依存する複雑な条件分岐に向いています。Workflows は数時間、数日、数週間実行できます。
両方を組み合わせる場合 は、高スループットの入口が複雑な処理につながるときです。たとえば Queue で受信注文をバッファし、コンシューマーが複数ステップのフルフィルメントが必要な注文ごとに Workflow インスタンスを作成できます。
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 });
},
};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>;詳細は Queues と Workflows を参照してください。
ある Worker が別の Worker を呼ぶ必要があるときは、公開 URL へ HTTP リクエストするのではなく サービスバインディング を使います。サービスバインディングは追加コストがなく、パブリックインターネットを経由せず、型安全な RPC に対応しています。
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 });
},
};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>;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>"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:[email protected]:5432/mydb",
});
await client.connect();
const result = await client.query("SELECT id, name FROM users LIMIT 10");
return Response.json(result.rows);
},
};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:[email protected]: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 を参照してください。
通常の Workers でも HTTP 接続を WebSocket にアップグレードできますが、永続状態とハイバネーションはありません。isolate が退去されると、接続を保持する永続アクターがないため、接続は失われます。信頼できる長寿命の WebSocket 接続には、Hibernation API 付きの Durable Objects を使います。Durable Objects は、オブジェクトがメモリから退去されても WebSocket 接続を開き続け、メッセージ到着時に自動で復帰します。
ハイバネーションを有効にするには、ws.accept() ではなく this.ctx.acceptWebSocket() を使います。オブジェクトを起こさない ping/pong ハートビートには setWebSocketAutoResponse を使います。
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);
}
}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 は、静的サイト、シングルページアプリケーション、フルスタックアプリを 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 はブラックボックスです。本番へデプロイする前に、ログとトレースを有効にします。間欠的なエラーが出たときに、すでに収集されているデータが診断に必要です。
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.01export 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");
},
};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 Logs と Traces を参照してください。
利用できるオブザーバビリティツールの全体像は Workers Observability を参照してください。
Workers はリクエスト間で isolate を再利用します。あるリクエストで設定した変数は、次のリクエストでも残っています。リクエスト間のデータ漏洩、古い状態、「Cannot perform I/O on behalf of a different request」エラーの原因になります。
状態は関数の引数で渡すか、env バインディングに保存します。モジュールレベルの変数には置かないでください。
// 🔴 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 };
}// 🔴 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 のエラー を参照してください。
await、return、または 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/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");
},
};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>;Workers ランタイムは、暗号処理向けに Web Crypto API を提供します。一意の識別子には crypto.randomUUID()、乱数バイトには crypto.getRandomValues() を使います。セキュリティに関わる用途で Math.random() を使ってはいけません。暗号学的に安全ではありません。
nodejs_compat が有効な場合、Node.js の node:crypto も完全にサポートされます。自分やライブラリが好みの API を使えます。
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 });
},
};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() を使います。長さの不一致で短絡評価してはいけません。先に両方の値を固定サイズのハッシュにエンコードします。
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;
}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() は、Worker が未処理例外を投げたときにリクエストをオリジンへ送る fail-open の仕組みです。オリジンサーバーからの移行中には役立つことがありますが、バグを隠し、デバッグを難しくします。代わりに、明示的な try...catch と構造化されたエラーレスポンスを使います。
// 🔴 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" };
}// 🔴 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 パッケージは、Workers ランタイム内でテストを実行します。テスト中に実際のバインディング(KV、R2、D1、Durable Objects)へアクセスできます。未対応 API や不足している互換性フラグなど、Node.js ベースのテストが見逃す問題を検出できます。
既知の落とし穴が 1 つあります。Vitest プラグインは nodejs_compat を自動注入するため、Wrangler 設定にフラグがなくてもテストは通ります。コードが Node.js 組み込みモジュールに依存する場合は、wrangler.jsonc に nodejs_compat があることを必ず確認してください。
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();
});
});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 でのテスト を参照してください。
- Rules of Durable Objects: 状態を持ち、協調するアプリケーション向けのベストプラクティス。
- Rules of Workflows: 耐久性のある複数ステップの Workflows 向けのベストプラクティス。
- プラットフォームの制限: CPU 時間、メモリ、サブリクエストなどの制限。
- Workers のエラー: エラーコードとデバッグの指針。