Advanced (更新: 2026/6/2)

Claude Codeでキューシステムを実装する実践ガイド

ジョブキューの基礎から再試行・DLQ・監視まで、Claude Codeで実装する実践ガイド。

Claude Codeでキューシステムを実装する実践ガイド

Claude CodeでWebサービスを作ると、最初はAPIの中で全部を同期的に処理したくなります。問い合わせフォームを受けたらメールを送り、画像を受けたらリサイズし、決済に失敗したら再請求メールを出す。小さな検証ならそれでも動きますが、本番では外部APIの遅延、タイムアウト、ユーザーの連打、再送、障害復旧が重なります。

そこで必要になるのがジョブキューです。ジョブキューは「あとで確実に処理したい仕事」を一度箱に入れ、別のワーカーで順番に処理する仕組みです。Claude Codeに実装を任せるときも、「キューを作って」だけでは足りません。producer(仕事を入れる側)、consumer(仕事を取り出す側)、message payload(処理に必要なデータ)、visibility timeout(処理中の一時ロック時間)、retry(再試行)、dead-letter queue(失敗した仕事の隔離場所)、idempotency(同じ仕事が2回来ても結果を二重にしない性質)まで明示する必要があります。

この記事では、ライブのRedisやAWS環境を使わず、依存なしのNode.jsだけでキューの動きを再現します。本番でSQS、RabbitMQ、BullMQを使う前に、まず「何を守るべきか」を手元で理解するための実践ガイドです。

全体像

キューシステムは、速くするためだけの道具ではありません。APIレスポンスを早く返す、外部サービスの障害を吸収する、失敗した仕事をあとから調査する、処理量を制限してDBやメール配信サービスを守る、という複数の役割を持ちます。

flowchart LR
  A["Producer<br/>API, cron, webhook"] --> B["Queue<br/>message payload"]
  B --> C["Consumer<br/>worker process"]
  C --> D["External service<br/>mail, image, billing"]
  C -- "retryable failure" --> B
  C -- "poison message" --> E["DLQ<br/>manual review"]
  C --> F["Metrics<br/>logs and alerts"]

用語を実務の言葉に置き換えると、次のようになります。

用語かんたんな意味実装で決めること
Producer仕事をキューに入れるAPIやバッチpayloadの形式、重複防止キー、優先度
Consumerキューから仕事を取るワーカー同時実行数、タイムアウト、失敗時の扱い
Message payloadワーカーが読む仕事の中身ID、種別、必要最小限のデータ、schema version
Visibility timeoutあるワーカーが処理中の仕事を他のワーカーから隠す時間p95処理時間より少し長くする
Retry一時的な失敗をやり直すこと最大回数、backoff、失敗理由の記録
DLQ何度やっても失敗した仕事の退避先誰が見るか、再投入する条件、通知先
Idempotency同じ仕事を再実行しても結果が二重にならない性質業務IDの一意制約、処理済みテーブル
Backpressure処理しきれない量を抑える仕組みconcurrency、rate limit、受付停止条件
Monitoring何が詰まっているか見える状態キュー深さ、最古ジョブ年齢、失敗率、DLQ件数

この表をClaude Codeへの依頼に含めるだけで、薄いサンプルから本番向けの設計に近づきます。

よくあるユースケース

1つ目はメール送信キューです。問い合わせ完了メール、パスワードリセット、請求失敗通知、ステップメールは、APIレスポンスの中で送るよりキューに積むほうが安全です。送信設計はClaude Codeでメール自動化SendGridメール実装も合わせて確認してください。メール本文そのものやAPIキーをpayloadへ入れず、templateId、userId、deliveryIdのような参照IDを入れるのが基本です。

2つ目は画像・動画処理キューです。アップロード直後にサムネイル、WebP変換、ウイルススキャン、字幕生成を同期処理すると、ユーザーは待たされます。キュー化すれば、先に「受付完了」を返し、処理結果はポーリングやWebhookで返せます。ただしCPUを使い切りやすいため、同時実行数を無制限にしないことが重要です。

3つ目は請求リトライキューです。カード決済、請求書発行、サブスクリプション更新は、外部決済APIの一時障害やカード会社の遅延で失敗します。ここで無限リトライをすると、同じ顧客へ何度も請求したり、プロバイダーのrate limitに引っかかったりします。有限回数、指数backoff、DLQ、手動確認の導線を用意します。

4つ目はリードエンリッチメントやレポート生成キューです。問い合わせ後にCRMへ登録し、会社情報を補完し、営業向けレポートを作る処理は、ユーザーの画面表示とは切り離せます。イベント設計全体はイベント駆動アーキテクチャ、詰まりを見つける方法はログ・モニタリング、payloadに機密情報を入れない設計はセキュリティベストプラクティスにつなげて考えると運用しやすくなります。

実装例1: 依存なしのインメモリキュー

最初の例は、producer、consumer、message payload、visibility timeout、backpressureを1つのファイルで再現します。保存してnode queue-basic-demo.mjsで実行できます。プロセスをまたいだ永続化はありませんが、キューの基本動作を理解するには十分です。

// queue-basic-demo.mjs
let nextJobId = 1;

class InMemoryQueue {
  constructor({ visibilityTimeoutMs = 800, maxInFlight = 2 } = {}) {
    this.visibilityTimeoutMs = visibilityTimeoutMs;
    this.maxInFlight = maxInFlight;
    this.ready = [];
    this.inFlight = new Map();
  }

  enqueue(type, payload) {
    const job = {
      id: `job-${nextJobId++}`,
      type,
      payload,
      attempts: 0,
      visibleAt: 0,
      lockedBy: null,
    };
    this.ready.push(job);
    return job.id;
  }

  receive(workerId) {
    this.requeueExpired();

    if (this.inFlight.size >= this.maxInFlight) {
      return null;
    }

    const job = this.ready.shift();
    if (!job) return null;

    job.attempts += 1;
    job.lockedBy = workerId;
    job.visibleAt = Date.now() + this.visibilityTimeoutMs;
    this.inFlight.set(job.id, job);

    return {
      id: job.id,
      type: job.type,
      payload: job.payload,
      attempts: job.attempts,
    };
  }

  ack(jobId) {
    this.inFlight.delete(jobId);
  }

  requeueExpired(now = Date.now()) {
    for (const [jobId, job] of this.inFlight.entries()) {
      if (job.visibleAt <= now) {
        this.inFlight.delete(jobId);
        job.lockedBy = null;
        this.ready.push(job);
      }
    }
  }

  stats() {
    this.requeueExpired();
    return {
      ready: this.ready.length,
      inFlight: this.inFlight.size,
    };
  }
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function produce(queue) {
  queue.enqueue("email.send", {
    deliveryId: "mail-1001",
    templateId: "welcome",
    userId: "user-42",
  });
  queue.enqueue("image.resize", {
    assetId: "asset-9001",
    sizes: [320, 768, 1280],
  });
  queue.enqueue("report.generate", {
    reportId: "weekly-2026-06-02",
    accountId: "acct-7",
  });
}

async function consume(queue, workerId) {
  for (let step = 0; step < 8; step += 1) {
    const job = queue.receive(workerId);

    if (!job) {
      console.log(`${workerId}: no job or backpressure`, queue.stats());
      await sleep(120);
      continue;
    }

    console.log(`${workerId}: started ${job.id}`, job.payload);
    await sleep(job.type === "image.resize" ? 300 : 90);
    queue.ack(job.id);
    console.log(`${workerId}: acked ${job.id}`, queue.stats());
  }
}

async function main() {
  const queue = new InMemoryQueue({
    visibilityTimeoutMs: 500,
    maxInFlight: 2,
  });

  produce(queue);
  await Promise.all([consume(queue, "worker-a"), consume(queue, "worker-b")]);
  console.log("final stats", queue.stats());
}

void main();

実務ではこのready配列がSQS、RabbitMQ、Redisなどの永続ストアに置き換わります。大事なのは、「受け付けた仕事」「誰かが処理中の仕事」「時間切れで戻す仕事」を分けて扱うことです。

実装例2: ワーカーの冪等性ガード

キューは少なくとも1回処理される前提で設計します。つまり、同じジョブが2回来ることがあります。メールなら二重送信、請求なら二重課金、ポイント付与なら二重付与が起きます。次の例は、idempotencyKeyで処理済みを記録する最小パターンです。

// idempotent-worker-demo.mjs
const idempotencyStore = new Map();
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function withIdempotency(key, work) {
  const current = idempotencyStore.get(key);

  if (current?.status === "done") {
    return { skipped: true, result: current.result };
  }

  if (current?.status === "processing") {
    return { skipped: true, reason: "already processing" };
  }

  idempotencyStore.set(key, { status: "processing" });

  try {
    const result = await work();
    idempotencyStore.set(key, { status: "done", result });
    return { skipped: false, result };
  } catch (error) {
    idempotencyStore.delete(key);
    throw error;
  }
}

async function fakeSendEmail(payload) {
  await sleep(50);
  return {
    providerMessageId: `sg_${payload.deliveryId}`,
    sentToUserId: payload.userId,
  };
}

async function handleEmailJob(job) {
  const key = job.payload.idempotencyKey;
  if (!key) throw new Error("missing idempotencyKey");

  return withIdempotency(key, () => fakeSendEmail(job.payload));
}

async function main() {
  const original = {
    id: "job-1",
    payload: {
      idempotencyKey: "email:welcome:user-42",
      deliveryId: "mail-1001",
      userId: "user-42",
    },
  };

  console.log(await handleEmailJob(original));
  console.log(await handleEmailJob({ ...original, id: "job-1-redelivery" }));
}

void main();

本番ではMapではなくDBの一意制約、RedisのSETNX、決済プロバイダーのidempotency keyなどを使います。Claude Codeには「成功時だけ処理済みにする」「処理中ロックを置く」「失敗時はロックを解除する」「payloadにAPIキーやメール本文を入れない」と明示してください。

実装例3: retryとDLQ

一時的なネットワーク障害はretryで回復できます。しかし、schemaが壊れているpayload、存在しないユーザーID、権限のない操作は何度やっても成功しません。このようなpoison message(毒メッセージ、処理しても必ず失敗する仕事)を通常キューに戻し続けると、ワーカーが永遠に同じ失敗を繰り返します。

// retry-dlq-demo.mjs
let nextRetryJobId = 1;

class RetryQueue {
  constructor({ maxAttempts = 3 } = {}) {
    this.maxAttempts = maxAttempts;
    this.ready = [];
    this.delayed = [];
    this.dead = [];
    this.completed = [];
  }

  enqueue(payload) {
    this.ready.push({
      id: `retry-job-${nextRetryJobId++}`,
      payload,
      attempts: 0,
      runAt: Date.now(),
      lastError: null,
    });
  }

  moveReadyJobs(now = Date.now()) {
    const stillDelayed = [];
    for (const job of this.delayed) {
      if (job.runAt <= now) {
        this.ready.push(job);
      } else {
        stillDelayed.push(job);
      }
    }
    this.delayed = stillDelayed;
  }

  retryOrDeadLetter(job, error) {
    job.lastError = error.message;

    if (job.attempts >= this.maxAttempts) {
      this.dead.push(job);
      return;
    }

    const delayMs = 50 * 2 ** (job.attempts - 1);
    job.runAt = Date.now() + delayMs;
    this.delayed.push(job);
  }

  async drain(handler) {
    let idleRounds = 0;

    while (this.ready.length > 0 || this.delayed.length > 0) {
      this.moveReadyJobs();
      const job = this.ready.shift();

      if (!job) {
        idleRounds += 1;
        if (idleRounds > 100) throw new Error("drain timeout");
        await sleep(20);
        continue;
      }

      idleRounds = 0;
      job.attempts += 1;

      try {
        const result = await handler(job);
        this.completed.push({ id: job.id, result });
      } catch (error) {
        this.retryOrDeadLetter(job, error);
      }
    }

    return {
      completed: this.completed.length,
      dead: this.dead.length,
    };
  }
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function handler(job) {
  if (job.payload.kind === "poison") {
    throw new Error("invalid payload schema");
  }

  if (job.payload.kind === "flaky" && job.attempts < 2) {
    throw new Error("temporary provider timeout");
  }

  return `processed ${job.payload.kind}`;
}

async function main() {
  const queue = new RetryQueue({ maxAttempts: 3 });
  queue.enqueue({ kind: "normal" });
  queue.enqueue({ kind: "flaky" });
  queue.enqueue({ kind: "poison" });

  console.log(await queue.drain(handler));
  console.log(
    "dead letters",
    queue.dead.map((job) => ({
      id: job.id,
      attempts: job.attempts,
      lastError: job.lastError,
      payload: job.payload,
    }))
  );
}

void main();

retryは「根性で何度も回す」仕組みではありません。成功しそうな一時失敗だけを有限回数で試し、失敗が続いたらDLQへ送る仕組みです。DLQに入ったジョブは、アラート、調査、修正、再投入の流れまで決めて初めて意味があります。

運用チェックリスト

  • payloadにはjobIdtypeschemaVersion、業務ID、idempotency keyを入れる
  • payloadにAPIキー、アクセストークン、メール本文、カード情報、長文の個人情報を入れない
  • producer側でpayloadを検証し、壊れた仕事をキューに入れない
  • visibility timeoutをp95処理時間より長くし、長すぎる処理は進捗更新や分割を検討する
  • retry回数、backoff、jitter、DLQ移動条件を明文化する
  • consumerのconcurrencyをDB接続数、外部API rate limit、CPU使用率から決める
  • キュー深さ、最古ジョブ年齢、active件数、失敗率、DLQ件数、処理時間p95を監視する
  • DLQを誰が何分以内に見るか、再投入してよい条件は何かをrunbookに書く
  • 二重配送を前提に、メール・請求・ポイント付与・CRM登録に冪等性ガードを入れる
  • Claude Codeの差分レビューでは、成功パスだけでなく失敗パスと運用ログを見る

特にvisibility timeoutの設定ミスは目立ちません。短すぎると、まだ処理中の仕事が別ワーカーに再配送され、二重処理になります。長すぎると、ワーカーが落ちたときに仕事がなかなか戻らず、ユーザーから見ると処理が止まります。まず実測のp95を見て、余裕を持たせ、長時間ジョブは分割するのが現実的です。

Claude Codeへの依頼テンプレート

Claude Codeに頼むときは、実装対象だけでなく、失敗時の契約を渡します。たとえば次のように依頼します。

このリポジトリにメール送信キューを追加してください。APIは送信要求をDBへ保存し、キューにはdeliveryIdtemplateIdだけを入れます。ワーカーはidempotency keyで二重送信を防ぎ、最大3回だけ指数backoffでretryし、失敗が続いたらDLQテーブルへ移します。payloadにAPIキー、メール本文、個人情報を入れないでください。キュー深さ、最古ジョブ年齢、失敗率、DLQ件数をログまたはmetricsで見えるようにし、テストでは重複配送、poison message、visibility timeoutを確認してください。

この指示なら、Claude Codeは「動くサンプル」ではなく、レビュー可能な設計へ寄せやすくなります。既存のAPI設計、メール送信、監視、セキュリティの記事へ内部リンクを張っておくと、記事としても読者の次の行動が明確になります。

公式ドキュメントと本番選定

本番でどのキューを選ぶかは、既存インフラと運用チームで決まります。AWS中心ならAmazon SQS Developer Guideが第一候補です。標準キューは高スループットで、FIFOキューは順序や重複排除を扱いやすい一方、設計上の制約もあります。

メッセージングの柔軟性、ルーティング、pub/sub、オンプレやKubernetes運用を重視するならRabbitMQ documentationが候補になります。Node.jsアプリでRedisを既に運用しており、ジョブ管理、遅延実行、repeatable jobs、ダッシュボード連携を素早く作りたいならBullMQ documentationが現実的です。

ただし、ライブラリ名から決めるのは順番が逆です。まずpayload、冪等性、retry、DLQ、監視、権限、費用、チームの運用経験を決め、その制約に合うサービスを選びます。この記事のサンプルを依存なしにしたのは、SQSやBullMQを使う前に、失敗時の考え方を切り出して学ぶためです。

失敗例と落とし穴

もっとも多い失敗は、duplicate processingです。キューは「1回だけ処理される」と思い込まないでください。ネットワーク断、visibility timeout切れ、ワーカー再起動、プロバイダー側の再送で同じ仕事は来ます。冪等性キーと一意制約がないメール・請求・CRM登録は、本番で必ず事故ります。

次にpoison messageです。payloadのschemaが古い、必須IDがない、削除済みユーザーを参照している、外部APIの権限がない。このような仕事はretryしても成功しません。検証で弾く、失敗理由を保存する、DLQへ移す、再投入前に修正する、という流れが必要です。

無限リトライも危険です。外部APIの障害時に全ジョブが即時リトライすると、復旧直後のサービスへさらに負荷をかけます。指数backoffとjitterを入れ、最大回数を決め、キュー深さが増えたらproducer側で受付を抑えるbackpressureを入れます。

また、payloadにsecretsを入れる失敗もあります。キューはログ、DLQ、管理画面、ダンプ、サポート調査に残りやすい場所です。SendGrid APIキー、OAuth token、カード番号、長文の問い合わせ本文を入れると漏えい時の被害が大きくなります。payloadには参照IDだけを入れ、ワーカーが権限のある場所から必要データを取り直します。

研修・相談へのつなげ方

キューはコード量より運用設計で差が出ます。ClaudeCodeLabでは、既存リポジトリを題材に、Claude Codeへの指示、CLAUDE.mdの禁止事項、payload設計、DLQ runbook、監視メトリクス、レビュー観点まで一緒に整理できます。チームで非同期処理を導入する場合はClaude Code研修・導入相談から相談してください。個人で型を固める場合は、この記事のチェックリストをPRテンプレートや実装メモに貼って使うだけでも、かなり事故を減らせます。

まとめ

ジョブキューは、重い処理を後ろに回すだけの仕組みではありません。失敗を隔離し、再試行を制御し、二重処理を防ぎ、処理量を抑え、あとから調査できる状態を作るための本番基盤です。Claude Codeを使うなら、producer、consumer、payload、visibility timeout、retry、DLQ、idempotency、backpressure、monitoringを最初の依頼に入れてください。

この記事で紹介した内容を実際にMasaが手元のNode.jsで試した結果、3つのサンプルは外部サービスなしで構文確認と実行確認ができ、visibility timeout切れ、重複配送、poison messageの動きを短時間で再現できました。特に冪等性ガードを入れた例は、同じメールジョブが再配送されても二重送信にならないことをログで確認でき、Claude Codeに本番実装を依頼するときの前提説明として使いやすい形でした。

#Claude Code #ジョブキュー #非同期処理 #BullMQ #Redis
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。