Cloudflare Queues × Workers TypeScript で作る最小ジョブキュー【個人開発向け】
TL;DR(結論)
Cloudflare Queues + Workers を使うと、月数十万メッセージまで無料枠で動く非同期ジョブキューをTypeScriptだけで実装できます。
本記事では「Producer Worker がキューにメッセージを積む → Consumer Worker が受け取って処理する」最小構成を、ゼロから動かすまでの手順を解説します。
なぜ Cloudflare Queues を選ぶのか
個人開発でジョブキューが欲しくなる場面はたくさんあります。
- 画像のリサイズ・変換を非同期にしたい
- Webhook を受け取ってすぐ 200 を返し、重い処理は後回しにしたい
- 外部 API への過剰リクエストを平滑化したい
SQS や Cloud Tasks でも実現できますが、別クラウドのセットアップ・IAM・VPC 設定が増えます。
Cloudflare Queues は Workers エコシステムに閉じているため、wrangler 一本で完結するのが最大の利点です。
| 比較項目 | Cloudflare Queues | Amazon SQS | Cloud Tasks |
|---|---|---|---|
| 無料枠 | 100万msg/月(送受信合計) | 100万msg/月 | 100万操作/月 |
| セットアップ | wrangler のみ | AWS CLI + IAM | gcloud CLI + SA |
| 実行基盤 | Workers (エッジ) | Lambda 等と別管理 | Cloud Run 等と別管理 |
| TypeScript 型安全 | 公式型定義あり | @aws-sdk | @google-cloud |
注意: 無料枠の上限・料金は変更されることがあります。最新情報は Cloudflare Queues 公式ページ を必ず確認してください。
前提条件
- Node.js 18 以上
wranglerv3 以上(npm i -g wrangler)- Cloudflare アカウント(無料プラン可。ただし Queues は Workers Paid プラン $5/月 が必要)
wrangler login済み
Step 1: プロジェクト作成
npm create cloudflare@latest queues-demo -- --type=hello-world --ts
cd queues-demo
ディレクトリ構成はシンプルにこうします。
queues-demo/
├── src/
│ ├── producer.ts # キューへ送信する Worker
│ └── consumer.ts # キューから受信して処理する Worker
├── wrangler.toml
└── package.json
Step 2: Queue を作成する
wrangler queues create my-job-queue
成功すると Queue ID が発行されます。この ID は wrangler.toml で使います。
Step 3: wrangler.toml を設定する
Producer と Consumer を同一 Worker に同居させることも、別々の Worker に分けることもできます。
本記事では責務を明確にするため別々に定義します。
# wrangler.toml
name = "queues-producer"
main = "src/producer.ts"
compatibility_date = "2024-11-01"
[[queues.producers]]
queue = "my-job-queue"
binding = "MY_QUEUE"
# --- Consumer は別エントリポイントとして定義 ---
[[queues.consumers]]
queue = "my-job-queue"
# Consumer の設定は consumer Worker 側の wrangler.toml に書くのが推奨
Consumer 専用の wrangler.consumer.toml を用意します。
# wrangler.consumer.toml
name = "queues-consumer"
main = "src/consumer.ts"
compatibility_date = "2024-11-01"
[[queues.consumers]]
queue = "my-job-queue"
max_batch_size = 10 # 一度に受け取る最大メッセージ数
max_batch_timeout = 5 # 最大待機秒数(バッチが埋まらなくても処理を開始)
max_retries = 3 # 失敗時の最大リトライ回数
dead_letter_queue = "my-job-queue-dlq" # 失敗し続けたメッセージの送り先
Step 4: Producer Worker を実装する
src/producer.ts を作成します。
// src/producer.ts
export interface Env {
MY_QUEUE: Queue<JobMessage>;
}
// キューに積むメッセージの型を定義
export interface JobMessage {
jobId: string;
type: "resize-image" | "send-email";
payload: Record<string, unknown>;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
const body = await request.json<{ type: string; payload: Record<string, unknown> }>();
const message: JobMessage = {
jobId: crypto.randomUUID(),
type: body.type as JobMessage["type"],
payload: body.payload,
};
// キューへ送信(send は非同期で即座に返る)
await env.MY_QUEUE.send(message);
return Response.json({ ok: true, jobId: message.jobId }, { status: 202 });
},
};
ポイントは env.MY_QUEUE.send() の返却が Promise<void> であること。
HTTP レスポンスをすぐ返しながら、処理は Consumer に委ねるパターンが実現できます。
Step 5: Consumer Worker を実装する
src/consumer.ts を作成します。
// src/consumer.ts
import type { JobMessage } from "./producer";
export interface Env {
// Consumer はキューへの送信バインディングは不要
}
export default {
// queue ハンドラが Workers の特別なエントリポイント
async queue(
batch: MessageBatch<JobMessage>,
env: Env
): Promise<void> {
for (const message of batch.messages) {
try {
await processJob(message.body);
// 成功したら明示的に ACK する
message.ack();
} catch (err) {
console.error(`Job ${message.body.jobId} failed:`, err);
// 失敗したら NACK → リトライキューへ戻る
message.retry();
}
}
},
};
async function processJob(job: JobMessage): Promise<void> {
switch (job.type) {
case "resize-image":
// 画像リサイズの処理(R2 などと組み合わせる)
console.log(`Resizing image for job ${job.jobId}`);
break;
case "send-email":
// メール送信処理(MailChannels や SendGrid など)
console.log(`Sending email for job ${job.jobId}`);
break;
default:
throw new Error(`Unknown job type: ${(job as JobMessage).type}`);
}
}
message.ack() / message.retry() を明示的に呼ぶことで、処理の成否を Queues に伝えます。
ack() を呼ばないとタイムアウト後に自動でリトライされるため、冪等な処理を設計することが重要です。
Step 6: デプロイして動作確認
# Producer をデプロイ
wrangler deploy
# Consumer をデプロイ
wrangler deploy --config wrangler.consumer.toml
# テストメッセージを送信
curl -X POST https://queues-producer.<your-subdomain>.workers.dev \
-H "Content-Type: application/json" \
-d '{"type":"send-email","payload":{"to":"[email protected]"}}'
# → {"ok":true,"jobId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}
# Consumer のログをリアルタイム確認
wrangler tail --config wrangler.consumer.toml
wrangler tail でリアルタイムにログが流れ、Consumer が正しくメッセージを受け取れていることを確認できます。
よくあるハマりどころ
① Paid プランに入っていないと Queue が作れない
無料プランでは wrangler queues create が 403 を返します。Cloudflare ダッシュボードから Workers Paid プランに切り替えてください。
② Consumer が起動しない
wrangler.toml の queues.consumers の設定が Producer 側にも書かれていると競合することがあります。Consumer は専用の toml ファイルにまとめると管理しやすいです。
③ メッセージが重複して処理される
Queues の配信保証は at-least-once です。冪等キー(jobId など)でDB側に処理済みチェックを入れ、重複実行を防ぎましょう。
Cloudflare エコシステムとの組み合わせ例
[クライアント]
│ POST /upload
▼
[Producer Worker] ─── send ───▶ [Cloudflare Queue]
│ 202 Accepted │
▼ consume
[クライアントへ即返却] ▼
[Consumer Worker]
│
┌─────────┼─────────┐
▼ ▼ ▼
[R2] [D1/KV] [外部API]
R2(オブジェクトストレージ)、D1(SQLite)、KV をすべて Workers バインディングで扱えるため、外部サービスへの依存を最小化した個人開発スタックが完成します。
まとめ
| ステップ | やること |
|---|---|
| 1 | wrangler queues create でキューを作成 |
| 2 | wrangler.toml に producer / consumer バインディングを設定 |
| 3 | Producer で env.MY_QUEUE.send() を呼ぶ |
| 4 | Consumer の queue() ハンドラで message.ack() / retry() を制御 |
| 5 | wrangler deploy × 2 でデプロイ完了 |
Cloudflare Queues は設定・デプロイ・監視まで wrangler 一本で完結するのが個人開発に刺さる理由です。サーバーの管理ゼロで at-least-once のジョブキューが手に入るのは、小規模プロダクトには十分すぎるほどです。
個人開発をもっとスケールさせたい・副業・受託で収益化したいと考えているなら、スキルを体系的に整理する意味でプロの力を借りるのも手です。
{{A8:coconala}}
まずは無料枠の範囲でプロトタイプを作り、トラフィックが増えてから有料プランへ移行する、というステップが個人開発では現実的です。ぜひ試してみてください。