Cloudflare Queuesの仕組みと特徴——Cronと勘違いしやすい理由

Cloudflare Queuesは、名前や「バックグラウンドで何かを処理する」という説明だけを見ると、定期実行の仕組みだと誤解されがちです。しかし実態はCron Triggersとは発火の仕組みからして全く別物です。ここではQueues自体の仕組みと特徴を紹介しながら、なぜCronと混同しやすいのか、実際は何が違うのかを見ていきます。

Queuesの仕組み

Queuesはproducer(送信側)とconsumer(受信側)の2つのWorkerで構成されるメッセージキューイングシステムです。producer側でメッセージをenqueueすると、Cloudflareのキューにいったん積まれ、consumer側のWorkerがqueue()ハンドラでそれを受け取って処理します。

// wrangler.jsonc — producer側
{
  "queues": {
    "producers": [
      { "queue": "ingest-jobs", "binding": "INGEST_QUEUE" }
    ]
  }
}
// producer: メッセージをenqueueする
export const POST: RequestHandler = async ({ request, platform }) => {
  const job = await request.json();
  await platform!.env.INGEST_QUEUE.send(job);
  // enqueueした時点で即座にレスポンスを返せる
  return new Response(null, { status: 202 });
};
// wrangler.jsonc — consumer側
{
  "queues": {
    "consumers": [
      { "queue": "ingest-jobs", "max_batch_size": 10, "max_retries": 3 }
    ]
  }
}
// consumer: queue()ハンドラでメッセージを処理する
export default {
  async queue(batch: MessageBatch<IngestJob>, env: Env) {
    for (const message of batch.messages) {
      try {
        await processJob(message.body, env);
        message.ack();
      } catch (err) {
        message.retry();
      }
    }
  },
};

Queuesの特徴

  • イベント駆動: enqueueという「実際に起きた出来事」に反応して動きます。何かが起きるまでは何もしません。
  • バッチ処理: max_batch_sizemax_batch_timeoutの設定に応じて、複数のメッセージをまとめてconsumerに渡せます。1件ずつ処理を起動するオーバーヘッドを避けられます。
  • リクエストのライフサイクルから切り離される: producer側はenqueueした時点で処理を終えてレスポンスを返せるため、実際に重い処理はユーザーのリクエストを待たせません。
  • リトライとDead Letter Queueが組み込み: message.retry()で明示的に再試行を指示でき、max_retriesを超えたメッセージは設定していればDead Letter Queue(DLQ)に送られます。失敗時の扱いを自前で実装する必要がありません。

一見Cronに見えるけれど、実は全く違う

QueuesもCron Triggersも「リクエストの外側で非同期に何かをする」という点では似ているため、混同しやすいのですが、トリガーの性質が根本的に異なります。

  • Queues: 「メッセージがenqueueされた」というイベントに反応する
  • Cron: 「実世界の時計が指定時刻に一致した」という時刻の条件に反応する

この違いは、ローカル開発での挙動にそのまま表れます。

  • Queues(ローカル): メッセージをenqueueすると、Miniflareが自動的にconsumer Workerのqueue()ハンドラを呼び出します。手動で何かを叩く必要はなく、実際のイベント駆動の動きがそのままローカルで再現されます。
  • Cron(ローカル): scheduled()ハンドラは時計を見て自動発火することはありません。/__scheduledエンドポイントを自分で叩かない限り、永遠に呼ばれません。
# ローカルでCronトリガーを手動発火する
curl "http://localhost:8787/__scheduled?cron=*+*+*+*+*"

Queuesは「enqueueされた」という事実そのものに反応するだけなので、ローカルでもそのまま成立します。一方Cronは、wrangler devが時刻を監視する常駐スケジューラーを回しているわけではないため自動発火せず、そもそも本番側も最大15分の伝播遅延があるゆるい仕組みなので、ローカルで厳密に再現する意味自体が薄いという事情もあります。だからこそ「テスト用に手動で発火を模倣するエンドポイント」が用意されている、という設計になっています。

使い分けの基準

「定期的に何かをする」のがCron、「何かが起きたら非同期に処理する」のがQueues、と考えると整理しやすくなります。両者は排他的ではなく、Cronで定期的にジョブを洗い出してQueuesにenqueueする、という組み合わせもよく使われるパターンです。Alcogyでも、時刻起点でまとめて発生するジョブはCronで拾い上げ、それを実際に処理する部分はQueuesのconsumerに任せる、という形で役割を分けています。

Queueコンシューマーも通常のWorkerである以上、CPU時間制限(デフォルト30秒、limits.cpu_msで最大300秒まで引き上げ可能)は健在です。「Queuesに入れれば処理時間を気にしなくていい」わけではない点は注意が必要ですが、それはまた別の話として、Queues自体は「時刻ではなくイベントに反応する非同期処理の仕組み」だと理解しておくと、Cronとの混同を避けられます。