💡 Tips

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 QueuesAmazon SQSCloud Tasks
無料枠100万msg/月(送受信合計)100万msg/月100万操作/月
セットアップwrangler のみAWS CLI + IAMgcloud CLI + SA
実行基盤Workers (エッジ)Lambda 等と別管理Cloud Run 等と別管理
TypeScript 型安全公式型定義あり@aws-sdk@google-cloud

注意: 無料枠の上限・料金は変更されることがあります。最新情報は Cloudflare Queues 公式ページ を必ず確認してください。


前提条件

  • Node.js 18 以上
  • wrangler v3 以上(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":"test@example.com"}}'
# → {"ok":true,"jobId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}

# Consumer のログをリアルタイム確認
wrangler tail --config wrangler.consumer.toml

wrangler tail でリアルタイムにログが流れ、Consumer が正しくメッセージを受け取れていることを確認できます。


よくあるハマりどころ

① Paid プランに入っていないと Queue が作れない

無料プランでは wrangler queues create403 を返します。Cloudflare ダッシュボードから Workers Paid プランに切り替えてください。

② Consumer が起動しない

wrangler.tomlqueues.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 バインディングで扱えるため、外部サービスへの依存を最小化した個人開発スタックが完成します。

📚 おすすめ書籍

Cloudflareを用いたフルスタック開発入門

Cloudflareエコシステムを体系的に学ぶならこの一冊

Amazonで見る →

まとめ

ステップやること
1wrangler queues create でキューを作成
2wrangler.toml に producer / consumer バインディングを設定
3Producer で env.MY_QUEUE.send() を呼ぶ
4Consumer の queue() ハンドラで message.ack() / retry() を制御
5wrangler deploy × 2 でデプロイ完了

Cloudflare Queues は設定・デプロイ・監視まで wrangler 一本で完結するのが個人開発に刺さる理由です。サーバーの管理ゼロで at-least-once のジョブキューが手に入るのは、小規模プロダクトには十分すぎるほどです。

個人開発をもっとスケールさせたい・副業・受託で収益化したいと考えているなら、スキルを体系的に整理する意味でプロの力を借りるのも手です。
{{A8:coconala}}

まずは無料枠の範囲でプロトタイプを作り、トラフィックが増えてから有料プランへ移行する、というステップが個人開発では現実的です。ぜひ試してみてください。