◉CLAUDE LABEN
●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り45日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り45日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/API & SDK
⬡ API & SDK/2026-05-01中級

Claude API のリトライを書いたらレート制限が悪化した話 — Retry-After を読まないリトライの罠

Claude API でリトライを実装したのに 429 エラーがむしろ増えた、という相談をよく受けます。原因のほとんどは Retry-After ヘッダーの無視と、ジッターのない指数バックオフです。診断と修正手順をまとめました。

claude-api83retry6rate-limit7backofftroubleshooting61anthropic12

「リトライを入れたのに、なぜか 429 が増えた気がする」— Claude API を本番投入した方から、これを一番よく相談されます。私自身も、初めて運用していた API クライアントに「3 秒待って 3 回まで再送」というシンプルな実装を入れたとき、ピーク時間帯にレート制限ヒットがかえって増えた経験があります。

直感的には、リトライは「優しい挙動」であるはずです。失敗したら少し待って再送するだけなのに、なぜ事態が悪くなるのでしょうか。理由は単純で、Retry-After ヘッダーを読まずに固定間隔で再送するクライアントが複数走ると、サーバーから見れば「同期した波」になるからです。Anthropic のドキュメントでは「指数バックオフを推奨」とだけ書かれていますが、実際に効くのは「ジッター付き指数バックオフ + Retry-After 優先」の組み合わせです。

ここでは私が個人開発で複数のアプリに Claude API を組み込む過程で何度も踏んだ「リトライの罠」を、診断順に整理します。コード例はすべて Node.js(公式 SDK 不使用の最小構成)で示しますが、Python やその他の言語でも考え方はそのまま使えます。

まずは usage と response headers を見る

リトライ戦略の見直しは、コードを書く前にサーバーから何が返っているかを確認するところから始めます。429 が返ってきたとき、レスポンスには次のヘッダーが含まれていることがあります。

  • retry-after: 再試行までに最低限待つべき秒数(数値、または HTTP 日付)
  • anthropic-ratelimit-requests-remaining: 残りリクエスト数
  • anthropic-ratelimit-requests-reset: 残数がリセットされる UNIX 時刻
  • anthropic-ratelimit-tokens-remaining: 残りトークン数
  • anthropic-ratelimit-tokens-reset: トークン枠がリセットされる UNIX 時刻

最初にやるべき診断は、429 を受け取ったときにこれらの値を全てログに出すことです。私は最初これを見ていなかったので、「なんとなく混雑しているらしい」程度の認識でした。実際にログを取り始めると、ある特定のリクエストでだけトークン枠を一気に使い切っていることに気づきました。原因はキャッシュ無効な巨大プロンプトを並列送信していたことで、リトライ戦略の問題というより設計の問題でした。

// 失敗時に必ずヘッダーをログに出す(最低限のフィールドだけ抜粋)
async function callClaude(body) {
  const res = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "x-api-key": process.env.ANTHROPIC_API_KEY,
      "anthropic-version": "2023-06-01",
      "content-type": "application/json",
    },
    body: JSON.stringify(body),
  });
 
  if (!res.ok) {
    // ★ 429 / 529 の時は必ずヘッダーを保存して原因分析に使う
    console.error({
      status: res.status,
      retryAfter: res.headers.get("retry-after"),
      reqRemaining: res.headers.get("anthropic-ratelimit-requests-remaining"),
      reqReset: res.headers.get("anthropic-ratelimit-requests-reset"),
      tokRemaining: res.headers.get("anthropic-ratelimit-tokens-remaining"),
      tokReset: res.headers.get("anthropic-ratelimit-tokens-reset"),
      requestId: res.headers.get("request-id"),
    });
  }
  return res;
}

request-id も忘れずに記録してください。Anthropic サポートに問い合わせる必要が出たとき、これが無いと調査が進みません。

ありがちな悪手 1: 固定秒数で待つだけのリトライ

最初に書きがちなのが、こういうコードです。

// ❌ 悪い例: 何度失敗しても同じ間隔で再送する
async function retryFixed(body, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const res = await callClaude(body);
    if (res.ok) return res;
    if (res.status === 429 || res.status === 529) {
      await new Promise((r) => setTimeout(r, 3000)); // 常に 3 秒
      continue;
    }
    return res;
  }
  throw new Error("max retries exceeded");
}

これが本当に良くない理由は、複数のクライアントが同時に 429 を受けると、ちょうど 3 秒後に全員が同時に再送する点にあります。サーバー側から見れば「同期したスパイク」が等間隔で繰り返し来る状態になります。これがいわゆる thundering herd(雷のような群衆)問題で、リトライを書いたつもりが攻撃に近い波形を生み出すことになります。

ありがちな悪手 2: ジッターなしの指数バックオフ

「指数バックオフ」と聞いて、こう書く人も多いです。

// ❌ 微妙な例: ジッター無し
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s...
await new Promise((r) => setTimeout(r, delay));

固定秒よりはマシですが、全クライアントの待機時間が同じである以上、波形は同期したままです。Anthropic 側から見ると「皆が 1 秒後に押し寄せ → 失敗 → 皆が 2 秒後に押し寄せ → 失敗」という綺麗な階段状のスパイクが残ります。

私の経験上、ここを直すだけで 429 のピーク発生率が体感半分以下になりました。

正しい実装: Retry-After 優先 + Full Jitter 指数バックオフ

AWS Architecture Blog で紹介されている "Full Jitter" バックオフは、Claude API でも非常に有効です。考え方はシンプルで、待機時間の上限を指数的に増やしつつ、その範囲でランダムに選ぶだけです。

// ✅ 推奨: Retry-After を最優先 + Full Jitter
function computeBackoffMs(attempt, retryAfter) {
  // 1) サーバーが Retry-After を返したら最優先で従う
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (!Number.isNaN(seconds)) return seconds * 1000;
    // HTTP 日付形式の場合
    const dateMs = Date.parse(retryAfter);
    if (!Number.isNaN(dateMs)) return Math.max(0, dateMs - Date.now());
  }
  // 2) ヘッダー無しなら Full Jitter 指数バックオフ
  const cap = 60_000; // 上限 60 秒
  const base = 500;   // 初期 0.5 秒
  const exp = Math.min(cap, base * 2 ** attempt);
  return Math.floor(Math.random() * exp); // 0〜exp の範囲でランダム
}
 
async function retryWithBackoff(body, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const res = await callClaude(body);
    if (res.ok) return res;
 
    // 5xx と 429 のみリトライ。4xx 系(401, 400 等)は即時失敗
    const retriable = res.status === 429 || res.status === 529 || (res.status >= 500 && res.status < 600);
    if (!retriable) return res;
 
    const wait = computeBackoffMs(attempt, res.headers.get("retry-after"));
    console.warn(`[retry ${attempt + 1}/${maxRetries}] status=${res.status} waiting=${wait}ms`);
    await new Promise((r) => setTimeout(r, wait));
  }
  throw new Error("max retries exceeded");
}

期待する挙動は次の通りです。サーバーが「3 秒待って」と言えばその通りに従い、何も指示が無ければ 0〜0.5s → 0〜1s → 0〜2s → 0〜4s … と、各クライアントが独立した時間で再送するため、波形が散らばります。

見落としやすい注意点

実装を直してもまだ 429 が減らない場合、以下を順に疑ってください。

  1. 冪等性の欠如: 同じリクエストを 2 回送ると副作用が出る処理(DB 書き込み等)に Claude 応答を直結している場合、リトライがバグになります。message.id を使って重複検知するか、リクエスト側で UUID を発行して冪等キーを管理してください
  2. 並列度が上限を超えている: アカウント Tier ごとに同時リクエスト数の暗黙的な上限があります。バックオフを正しくしてもプール全体が制限を超えていれば、何度直しても改善しません。p-limit などで明示的に並列数を絞るのが最短です
  3. レスポンス未読のまま中断している: HTTP 接続を中断するとサーバー側ではリクエストが完了扱いされないことがあり、レート枠が無駄に消費されます。タイムアウトは AbortController で明示的に行い、応答がエラーでも res.body を読み切るのが安全です
  4. トークン枠の枯渇を 429 と勘違いしている: anthropic-ratelimit-tokens-remaining がゼロなら、リクエスト数を絞っても解決しません。プロンプトキャッシュ導入や入力長削減が効きます。プロンプトキャッシュの実装パターンは Claude API のプロンプトキャッシュがヒットしない時の診断手順 にまとめました

ストリーミング応答との組み合わせ

ストリーミングを使っている場合、リトライ判断は「最初の message_start イベントが返ってきたかどうか」で分岐します。ストリームの途中で切れたものを最初からやり直すと、トークンが二重に課金される上にレート枠も二度消費されます。途中切断時の安全な扱いは Claude API ストリーミングを本番で落とさない実装 で詳しくまとめていますので、ストリーミング派の方はそちらと合わせて読んでみてください。

リトライの設計を本格的に

新規プロジェクトに毎回入れている小さな共通モジュール

私は新しい Claude API 連携を始めるとき、必ず同じヘルパーモジュールを最初に入れています。中身は fetch のラッパーで、ここで紹介したリトライ戦略を実装し、concurrency オプションで内部的に並列数を絞り、構造化ログを他のテレメトリと同じ場所に流すだけです。だいたい 80 行程度の小さなコードですが、価値は「最初にデプロイした時点で 429 をまともに扱える」ことです。本番のトラフィックが来てから教訓を学び直す必要がなくなります。複数の Claude 連携を運用しているなら、この小さな抽象化を一度作ってしまうのがおすすめです。

次にやるべき一歩

まず、いま動いている Claude API クライアントの 429 ハンドラーを開いて、retry-after ヘッダーを読んでいるかだけ確認してください。読んでいなければ、上記の computeBackoffMs をコピーして組み込むのが最短です。それだけでピーク帯のエラー率は明確に下がります。並列度が原因で詰まっている場合は、続けて p-limit での並列制御を入れる、という順番で進めるのが安全でした。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

⬡ API & SDK2026-05-22
Claude の『このモデルは現在利用できません』が出る時に効くモデル選択フォールバックの設計
Claude API で『このモデルは現在利用できません(this model is currently unavailable)』と返ってきたとき、529 Overloaded やレート制限と同じ扱いにすると遠回りになります。エラーの種類を SDK の例外クラスで見分け、モデル ID をリクエスト単位で切り替えるフォールバックの実装と、思い込みで書きやすい落とし穴をまとめました。
⬡ API & SDK2026-04-30
Claude APIをCloudflare Queuesで非同期処理する:障害復旧と優先度制御を含めた本番設計
Claude APIをCloudflare Queuesで非同期処理する本番グレードのパイプライン設計を、Producer/Consumer両方の動作コード・障害時のリトライ戦略・優先度制御・コスト制御まで含めて解説します。
⬡ API & SDK2026-04-26
Claude API のプロンプトキャッシュで月額コストを半分にした実装メモ
長文のシステムプロンプトを毎リクエスト送る本番ワークロードでは、プロンプトキャッシュの設計次第で月額コストが大きく変わります。ヒット率の計測から始める手順、TTLの選択、トークン下限と4ブロック上限、toolsを1つ足してヒット率が0になった失敗談に加え、ヒット率から入力コストを逆算する計算と損益分岐(5分TTLで約22%、1時間TTLで約53%)まで、半年の運用実例でまとめました。