LLM Frontline
開発・エージェント

LLM APIのレート制限とリトライ設計|429・タイムアウト・二重実行を本番で捌く

ミナト開発・API担当
・ 約33分で読めます
LLM APIのレート制限とリトライ設計|429・タイムアウト・二重実行を本番で捌く

検証段階では一度も出なかった429が、本番でユーザーが増えた途端に出始める。これはLLMを組み込んだアプリでかなり高い確率で通る道です。しかも厄介なのは、あわてて「リトライ回数を増やす」対処を入れると、全クライアントが同じタイミングで再送してさらに429が増える、という悪化の仕方をすることです。流量制御は後付けが効きにくい部分で、リトライの回数よりも、締め切り・予算・冪等性という3つの枠組みを先に決めておくかどうかで結果が変わります。この記事では、各社の公開ドキュメントで確認できる仕様を土台に、本番でLLM APIを叩き続けるための設計を整理します。

レート制限は「1本の上限」ではない

最初に外しておきたい誤解は、レート制限を「1分あたり何回まで」という1本の数字だと思ってしまうことです。実際には複数の軸が同時にかかっていて、いずれか1つに触れた時点で429が返ります。

どの軸で制限されるか

主要3社の公開ドキュメントで確認できる制限軸は次のとおりです。

プロバイダ主な制限軸備考
Anthropic (Claude)RPM(1分あたりリクエスト数)、ITPM(1分あたり入力トークン)、OTPM(1分あたり出力トークン)モデルクラスごとに設定され、いずれかを超えると429
OpenAIRPM、RPD(1日あたりリクエスト数)、TPM、TPD、IPM(1分あたり画像数)など最初に到達した制限が効く。一部のモデルファミリーは制限を共有する
Google (Gemini)RPM、TPM(入力トークン)、RPDモデルによってはTPD(1日あたりトークン)やIPMも使われる

ポイントは、リクエスト数とトークン数が別々にカウントされることです。「1リクエストあたりの入力を大きくして呼び出し回数を減らす」という最適化は、RPMには効きますがTPM(ITPM)には効きません。逆に短いリクエストを大量に投げる構成は、トークン制限に余裕があってもRPMで詰まります。自分のワークロードがどちらの軸に近いかを把握しないまま上限緩和を申請しても、詰まる場所は変わらないままです。

Anthropicのドキュメントには、カウントの細部についても記述があります。OTPMは実際に生成された出力トークンをリアルタイムに数えるもので、リクエストで指定したmax_tokensの値そのものは算入されません。また多くのモデルでは、プロンプトキャッシュの読み出し分(cache_read_input_tokens)はITPMに算入されず、input_tokensとcache_creation_input_tokensだけが数えられます。ただしClaude Haiku 3.5は例外で算入されるとされているので、モデルによる、という理解が正確です。

誰の単位でかかるか

上限が「誰に対して」設定されているかも重要です。ここを取り違えると、対策そのものが空振りします。

  • OpenAI: 組織レベルとプロジェクトレベルで定義され、ユーザ単位ではないと明記されています。
  • Anthropic: 組織単位が基本です。加えてワークスペース単位に組織より低い上限を任意設定でき、制限の種類(RPM/ITPM/OTPM)ごとに設定します。組織全体の上限は常に優先して適用されます。
  • Gemini: APIキー単位ではなくプロジェクト単位で適用されます。日次クォータは太平洋時間の深夜にリセットされます。

つまり3社とも、上限の単位はAPIキーそのものではありません。「429が出たのでキーを増やす」という対処は、単位が組織やプロジェクトである限り効かない可能性が高い、ということです。

tierとバケットの分かれ方

利用量や支払い実績に応じた段階(tier)があり、tierによって上限が変わります。Anthropicの使用tierはStart / Build / Scale / Customという名称で、GeminiはFree / Tier 1 / Tier 2 / Tier 3です。名称よりも押さえたいのは、バケット(上限の器)が用途ごとに分かれていることです。OpenAIのBatch APIは標準のモデル別レート制限とは別のプールで動くとされ、GeminiのBatch APIも非バッチ呼び出しとは別の制限を持ち、同時バッチリクエスト100件、モデルごとのBatch enqueued tokens(全アクティブバッチ合計でキューに入れられるトークン数)という形で管理されます。同期呼び出しが上限に張り付いていても、バッチ側にはまだ余裕がある、という状況が起こりえます。

数値表を覚えても意味がない理由

各社のドキュメントにはtier別の具体的な上限値が掲載されていますが、この記事ではあえて数値を転記しません。改定が頻繁だからです。実務で見るべきは、管理コンソールに表示される自分の組織の現在値と、レスポンスヘッダが返す残量です。

もうひとつ、数値だけを見ていると気づけない挙動があります。Anthropicはトークンバケット方式でレート制限を行い、容量は固定間隔でリセットされるのではなく継続的に補充されると説明しています。そのうえで「60RPMが1リクエスト/秒として運用されることがある」と明記されています。つまり1分の合計としては上限内でも、先頭の数秒にまとめて送れば429になりえます。バーストを平らにならすのは、上限緩和よりも先にやる仕事です。

メモ

急激にトラフィックを増やしたときにも429は出ます。Anthropicはacceleration limitsという仕組みに言及しており、対策として「トラフィックを段階的に増やすこと」を挙げています。キャンペーンや一斉バッチ投入のように、ゼロから一気に立ち上げる予定があるなら、事前に段階的な立ち上げを設計しておいてください。

何が起きたかを、ヘッダとエラーで見分ける

「APIがエラーを返した」で止めずに、何の制限に触れたのかを機械的に判別できるようにします。

429で返ってくるヘッダを読む

OpenAIはレート制限に関するヘッダとして、x-ratelimit-limit-requests / x-ratelimit-limit-tokens / x-ratelimit-remaining-系 / x-ratelimit-reset-系と、プロジェクト単位のx-ratelimit-系-project-tokens、そしてRetry-Afterを返します。Anthropicはretry-afterのほか、anthropic-ratelimit-requests-limit / -remaining / -reset、anthropic-ratelimit-tokens-系、anthropic-ratelimit-input-tokens-系、anthropic-ratelimit-output-tokens-系を返し、reset系の値はRFC 3339形式です。tokens系は現在もっとも厳しい制限の値を表示する、という注記もあります。

なおHTTPヘッダ名は大文字小文字を区別しないため、Anthropicの小文字表記とOpenAIのRetry-After表記の違いは実装上は問題になりません。これらのヘッダをログに残しておくと、429が出たときに「リクエスト数で詰まったのか、入力トークンで詰まったのか」を推測せずに判定できます。逆に残していないと、上限緩和を申請する材料も作れません。

429・5xx・504・クライアント側タイムアウトは別物

同じ「失敗」でも意味がまったく違います。Claude APIのエラーは、429 rate_limit_error、500 api_error、504 timeout_error、529 overloaded_errorなどに分かれています。529は全ユーザ横断でAPIが高負荷になっている場合に発生するとされ、500については「指数バックオフでリトライし、それでも続くならrequest IDを添えてサポートへ」と案内されています。

これらとまったく別なのが、クライアント側のタイムアウトです。サーバが何も返していないのではなく、こちらが待つのをやめただけかもしれません。サーバ側では生成が続き、課金も発生している可能性があります。この違いは後述の「二重実行」で効いてきます。

再試行してよいエラー、してはいけないエラー

Gemini APIの公式トラブルシューティングは、429・408・5xxのような一時的エラーだけをリトライし、400や403のようなクライアントエラー(不正なAPIキーや文法の誤り)はリトライしないと明記しています。OpenAIも、自前実装では課金エラーのように一時的でないエラーを再試行しないことを推奨しています。方針は共通です。

分類扱い
再試行してよい429、408、500系、503、529、接続エラーバックオフ+ジッタで再試行。回数と締め切りの上限内で
再試行しても無駄400(リクエスト不正)、401、403、404直さない限り何度送っても同じ。即座に失敗として扱う
判断が要る504、クライアント側タイムアウト副作用の有無で決める。冪等化できていれば再試行可

失敗したリクエストも上限を消費する

見落としやすい仕様として、OpenAIは「失敗したリクエストも1分あたりの上限に算入される」と明記しています。つまり429が返ってきたリクエストを間髪入れずに送り直すと、上限の消費だけが進んで永遠に通らない、という状態を自分で作れてしまいます。バックオフは礼儀ではなく、通すための必須条件です。

あわせて読みたい

LLMアプリのオブザーバビリティ|トレーシングとログ設計で「なぜこの出力になったか」を後から追えるようにする

リトライは「回数」ではなく「予算」と「締め切り」で設計する

ここが本記事の中心です。リトライ設計を「最大何回」だけで語ると、ほぼ確実に破綻します。

指数バックオフだけでは足りない。ジッタを入れる

複数のクライアントが同時に429を受け取ると、同じアルゴリズムで同じ秒数だけ待ち、同じ瞬間に再送します。バックオフしているのに衝突が続く、いわゆる同期の問題です。AWSのアーキテクチャブログの検証では、バックオフのみだとクライアントの再試行が固まる一方、ジッタ付きの手法(Full JitterとDecorrelated Jitter)は総処理量を半分以下に減らすと報告されています。Full Jitterは総呼び出し回数がより少なく、Decorrelated Jitterは完了がわずかに速く、中間のEqual Jitterはどちらにも劣るという結果です。OpenAIの公式ドキュメントも「Retry-Afterの秒数以上待ち、複数クライアントが同時に再試行しないよう小さなランダム遅延(ジッタ)を足す」と書いています。実装コストがほぼゼロで効果が大きい、最初に入れるべき対策です。

retry-afterを尊重する

Retry-AfterヘッダはRFC 9110の10.2.3で定義されており、値はHTTP-dateかdelay-seconds(非負の十進整数の秒)のどちらかです。数値だけを想定してパースすると日付形式で落ちるので、両方を受け付ける実装にします。

一方、429そのものはRFC 9110ではなくRFC 6585の第4節で定義されています。そこではRetry-Afterヘッダを含めてもよい(MAY)とされており、必ず付くとは限りません。したがって「retry-afterがあればそれを最優先し、無ければ自前の指数バックオフ+ジッタにフォールバックする」が正しい実装です。

デッドラインを先に決め、その内側にリトライを収める

リトライを増やせば成功率は上がりますが、その分だけ待ち時間が伸びます。対話UIで20秒待たされるくらいなら、5秒で諦めて「混み合っています」と返すほうがよい場面は多いはずです。順序を逆にしてください。まず「この処理は全体で何秒までなら許容できるか」というデッドラインを決め、リトライはその内側に収める。残り時間がバックオフ待機より短いなら、待たずにその場で諦める。これができていないと、上位のタイムアウトに切られるまで無駄に待つことになります。

リトライ予算を持つ

Google SRE Bookの「Handling Overload」は、リクエスト単位のリトライ上限を3回とし、加えてクライアント単位で「リトライ数/総リクエスト数が10%」を超えないリトライ予算を持つ運用を紹介しています。これによりリトライによる負荷増は3倍ではなく約1.1倍に抑えられる、という説明です。個々のリクエストから見ると「3回まで再試行できる」ですが、全体が失敗しているときは予算が枯れて再試行が止まる。この二段構えが、雪崩を防ぐ肝になります。

同書はほかにも、サーバ側が過負荷を検知したら「overloaded; don't retry」を返す、リトライは呼び出し階層の直上1層だけが行い多層リトライによる指数的な増幅を避ける、といった原則を示しています。LLM APIを呼ぶ側でも、そのまま使える考え方です。

擬似コード: デッドライン付きリトライ

方針をコードにすると次のような形です。言語やHTTPクライアントによらない骨格として読んでください。

# 擬似コード。締め切り(deadline_at)を先に決め、リトライはその内側に収める
def call_with_deadline(req, deadline_at, max_attempts=3, base=0.5, cap=8.0):
    attempt = 0
    while True:
        attempt += 1
        remaining = deadline_at - now()
        if remaining <= 0:
            raise DeadlineExceeded()
        try:
            # 1回あたりのタイムアウトも、残り時間より長くしない
            return http_post(req, timeout=min(remaining, per_try_timeout))
        except RetryableError as e:          # 429 / 408 / 5xx / 接続エラー
            if attempt >= max_attempts or not retry_budget.allow():
                raise                         # 回数上限、またはクライアント全体の予算切れ
            # retry-after があれば最優先。無ければ Full Jitter でばらす
            wait = e.retry_after or random.uniform(0, min(cap, base * 2 ** (attempt - 1)))
            if now() + wait >= deadline_at:   # 待つと締め切りを超えるなら、待たずに諦める
                raise DeadlineExceeded()
            sleep(wait)
        except FatalError:                    # 400 / 401 / 403 など。再送しても通らない
            raise

行数は少ないですが、入っている判断は4つです。残り時間の確認、retry-afterの優先、ジッタ付きバックオフ、予算による停止。回数だけを増やす実装との差はここにあります。

注意

SDKの自動リトライと自前のリトライを二重にかけると、実際の試行回数は掛け算になります。たとえばSDK側が2回、アプリ側が3回リトライすると最大6回です。どちらか一方に寄せてください。openai-pythonは既定で2回まで自動リトライし、対象は接続エラー・408・409・429・500以上とされています。Anthropicの公式SDKも、接続エラー・レート制限・5xxを既定で2回まで指数バックオフでリトライし、retry-afterヘッダがあれば尊重するとされています。いずれもクライアントのオプションで変更・無効化できます。

ストリーミングのタイムアウトは3つに分けて持つ

タイムアウトを1つの数字で持っていると、ストリーミングでは必ず困ります。分けて持つべきなのは次の3つです。

  1. 接続タイムアウト: TCP接続とTLSハンドシェイクが確立するまで。ここは短くてよく、数秒で十分です。
  2. 初回トークンまでの時間(TTFT): 200が返ってから最初のチャンクが届くまで。体感速度を決める部分で、ここが伸びたら混雑を疑います。
  3. 全体タイムアウト(と、チャンク間のアイドルタイムアウト): 生成完了までの上限。長文生成では長めに、対話では短めに設定します。

openai-pythonの既定のリクエストタイムアウトは10分で、httpx.Timeoutによりconnect/read/writeを個別に指定できるとされています。タイムアウトしたリクエストも既定で2回リトライされる点は注意が必要です(SDKのバージョンで変わりうるので、採用時に手元のドキュメントで確認してください)。OpenAIのflex処理(service_tier="flex")のドキュメントでは、応答が遅くなるためクライアントのタイムアウトを既定の10分から15分(900秒)へ引き上げる例が示されています。

Anthropicは、10分を超えるような長時間リクエストにはストリーミングのMessages APIかMessage Batches APIの利用を推奨しています。理由として、ネットワークによってはアイドル接続が切断されリクエストが失敗・タイムアウトすることを挙げ、直接API連携する場合はTCP keep-aliveの設定を勧めています。

200で始まったストリームが、途中で壊れることがある

ストリーミング特有の落とし穴として、APIが200を返した後にエラーが発生するケースがあります。Anthropicのドキュメントは、この場合は通常のエラーハンドリング機構に従わないと明示し、高負荷時にoverloaded_error(非ストリーミングならHTTP 529相当)がerrorイベントとして流れてくる例を挙げています。ストリームには任意個数のpingイベントも含まれます。

つまり、ステータスコードだけを見て成功と判定する実装は、ストリーミングでは正しくありません。イベントを最後まで読み、終端イベントを受け取ったかどうかで成否を判定する必要があります。200なのに失敗している呼び出しをエラー率に載せられるかどうかは、観測側の設計にも影響します。

中断したストリームをどう扱うか

途中で切れたときの選択肢は、捨てて最初からやり直す、部分応答を保存して続きを頼む、部分応答のままユーザーに見せる、の3つです。Anthropicは中断したストリームの復旧策として、受信済みの部分応答を保存して継続リクエストを組む方法を案内しています。Claude 4.5以前は部分応答をassistantメッセージとして継続し、Claude 4.6以降は部分応答を含むuserメッセージで「続きから続けて」と指示する形です。ただしtool_useとthinkingのブロックは部分復旧できないとされています。

実務では、ツール呼び出しを含むエージェント的な処理では素直にやり直し、長文生成では継続を試す、という切り分けが扱いやすいです。

二重実行(重複課金・重複副作用)への備え

リトライを入れた瞬間に、新しい問題が生まれます。「返ってこなかっただけで、実は処理済み」かもしれない、という問題です。

推論APIには汎用の冪等性キーが用意されていないことが多い

決済APIのようにIdempotency-Keyヘッダを付ければ済む、と考えたくなりますが、LLMの推論APIではそうなっていないことが多いです。OpenAIのAPIリファレンス総論には、サーバが生成するx-request-idヘッダと、クライアントが任意に付与できるX-Client-Request-Idヘッダの説明はありますが、推論API向けのIdempotency-Keyの記載はありません(本番ではrequest IDをログに保存することが推奨されています)。結論としては、冪等性はアプリ側で持つのが基本になります。

アプリ側に冪等性台帳を持つ

やることは単純で、業務上の一意キー(たとえば「このチケットIDの要約」「この注文の分類」)を決め、その状態と結果を自前のテーブルで管理します。

# 擬似コード。request_key -> 状態と結果 の台帳で二重実行を防ぐ
def run_once(request_key, work):
    row = ledger.insert_if_absent(request_key, status="in_progress")  # 一意制約で競合を弾く
    if row.status == "done":
        return row.result            # 既に完了している。再実行もLLM呼び出しもしない
    if row.status == "in_progress" and not row.is_stale():
        raise AlreadyRunning()       # 別ワーカーが処理中。二重起動を拒否する
    result = work()                  # ここでLLM呼び出しとツール実行を行う
    ledger.update(request_key, status="done", result=result)
    return result

is_staleの判定(処理中のまま何分放置されたら再実行を許すか)は、LLM呼び出しの最大所要時間より十分に長く取ります。短すぎると、遅いだけの処理を二重に走らせます。

ツール呼び出し側を冪等にする

エージェント構成では、LLMの呼び出しそのものより、ツールの副作用のほうが深刻です。メール送信、チケット作成、在庫引き当て、外部APIへの書き込み。これらは「同じ引数で2回呼ばれても1回分の効果しか出ない」ように作ります。実装としては、ツール呼び出しIDや業務キーを送信先の重複排除キーに渡す、書き込み前に存在確認する、という形が定番です。LLMは同じツールを繰り返し呼ぶことがあるので、リトライが無くても冪等化しておく価値があります。

タイムアウト後の再送は「処理済み」かもしれない

クライアント側でタイムアウトしたとき、サーバ側では生成が完了していた可能性があります。その場合、再送すると2回分の課金が発生します。1回あたりの金額は小さくても、混雑して全体が遅くなっているときにこれが起きると、コストは静かに倍増します。冪等性台帳を持っておけば、少なくともアプリ側の副作用が2回起きることは防げます。

ヒント

バッチAPIを使う場合、リクエストに付ける識別子を業務側の冪等キーとして流用できます。AnthropicのMessage Batches APIは各リクエストに1〜64文字の一意なcustom_id(パターンは英数字とアンダースコア、ハイフン)を要求します。ここに自分の業務キーを入れておけば、結果を取り込むときの突き合わせと重複排除が同じキーでできます。

あわせて読みたい

LLMのコスト管理|トークン課金の考え方と削減の定石

流量そのものを減らす

リトライの精緻化は対症療法です。そもそも同期呼び出しの流量を減らせるなら、そちらのほうが効きます。

バッチAPIへ逃がす

即時性が要らない処理は、非同期のバッチAPIに寄せるのが第一手です。3社とも同様の仕組みを持っています。

  • Anthropic Message Batches API: 非同期処理でコストを50%削減。多くのバッチは1時間以内に完了し、結果は全件完了時か24時間経過時のいずれか早い方で取得できます。24時間内に完了しないバッチは期限切れになり、期限切れ(expired)のリクエストは課金されません。
  • OpenAI Batch API: 1バッチ最大50,000リクエスト、完了ウィンドウは24時間(多くはより早く完了)、同期APIに対して50%の割引。標準のモデル別レート制限とは別のプールで動きます。
  • Gemini Batch API: 24時間の完了目標(多くはより早い)で、標準コストの50%。20MB未満はインラインリクエスト、大きいものはFile API経由のJSONL(最大2GB)で投入します。

夜間の一括分類、既存データの再処理、レポート生成といった処理をバッチへ移すだけで、日中の同期側の余裕が変わります。コストが半分になるのも見逃せません。

プロンプトキャッシュで制限の消費を減らす

プロンプトキャッシュはコスト削減の文脈で語られがちですが、流量制御の観点でも効きます。前述のとおり、多くのClaudeモデルではキャッシュ読み出し分がITPMに算入されません(モデルによる点には注意)。同じ前置きを何度も送る構成では、キャッシュが効いている分だけトークン制限の消費が減り、実効スループットが上がります。仕組みそのものは別記事で扱っているので、ここでは制限の消費を減らす手段でもある、という点だけ押さえてください。

あわせて読みたい

プロンプトキャッシュ(Prompt Caching)とは|LLM APIのコストとレイテンシを下げる仕組みと実務

キュー+ワーカーで平準化する

3社とも、同時実行数(concurrency)を一次的な制限軸として公開しているわけではありません(公式に確認できた同時実行系の数値は、GeminiのBatch APIにおける同時バッチリクエスト100件です)。だからこそ、同時実行数は自分で決める変数になります。アプリから直接APIを叩く構成をやめ、キューに積んでワーカーが引く構成にすると、ワーカー数という形で送出レートを自分の手で握れます。急なスパイクはキューの長さに現れるだけで、APIへの送出は一定に保たれます。前述のバースト問題(1分の合計は上限内でも先頭に固まれば429)への対処としても、これがいちばん素直です。

優先度を分ける

すべてのリクエストが同じ緊急度ではありません。対話系は即時、社内バッチは後回し、という分離をキュー側で表現します。プロバイダ側にもサービスティアの概念があり、OpenAIはpriority / 標準 / flex / batchという選択肢を提供しています(flexはBatch API相当の料金で応答が遅く、リソース不足時は429 Resource Unavailableが返り、その場合は課金されないとされています。対処として指数バックオフでの再試行か、service_tierを"auto"にして標準処理で再試行することが挙げられています)。

AnthropicのサービスティアはPriority、Standard、Batchの3種で、service_tierパラメータは"auto"(既定)と"standard_only"を受け付けます。ただしPriority Tierの容量コミットメントは新規購入ができなくなっており、既存契約の組織のみ契約終了まで利用できるとされています。優先度分離は、プロバイダの機能に頼りきるのではなく、自分のキュー設計で持っておくほうが安全です。

送る前に落とす

最後の手段として、クライアント側のスロットリングも用意しておきます。上限に近づいたら、新規の非優先リクエストを受け付けずにその場で断る。429を受けてから捌くより、そもそも送らないほうが全体は速く回ります。

監視すべき指標と、超えたときの縮退運転

流量制御は、数字を見ていないと調整できません。ここでは見るべき指標の列挙にとどめます。計装のやり方は観測の記事に譲ります。

指標見る理由
429率(モデル別・機能別)制限に触れている頻度。どの軸で触れたかはヘッダから分類する
リトライ後の最終成功率429が出ていても最終的に通っているなら、ユーザー影響は限定的
p95/p99レイテンシリトライを増やすと真っ先に悪化する。平均では見えない
デッドライン超過率諦めた割合。ここが増えたら上限緩和かキュー側の調整が要る
TPM/ITPM消費率上限に対する余裕。リクエスト数だけ見ていると気づけない
待ち行列長と滞留時間送出を絞っている場合、詰まりはキュー側に現れる
ストリーム中断率200で始まって完走しなかった割合

しきい値は絶対値ではなく、前週同時刻比や移動平均からの乖離で置くと、トラフィックの変動に振り回されにくくなります。

縮退運転は、順番を先に決めておきます。おおむね次の順で効きます。

  1. 1

    出力長を絞る

    max_tokensを下げる、要約の長さを短くする。OTPM側の圧力が下がり、レイテンシも改善します。
  2. 2

    モデルをフォールバックする

    上位モデルから軽量モデルへ切り替えます。品質は落ちるので、切り替えた事実をログに残し、ユーザーに見せるかどうかも決めておきます。
  3. 3

    非優先の機能を止める

    サジェスト、自動要約、先読みといった「あると便利」な呼び出しから止めます。
  4. 4

    キューへ退避する

    即時応答を諦め、完了後に通知する形へ切り替えます。
  5. 5

    受付を止めて明示する

    最後は正直に混雑を伝えます。失敗を黙って返すとユーザーがリロードを連打し、負荷が増えます。

あわせて読みたい

LLMアプリのオブザーバビリティ|トレーシングとログ設計で「なぜこの出力になったか」を後から追えるようにする

よくある失敗パターン

実際に踏みやすい順に挙げます。

  • 全クライアントが同じ秒数で再試行して雪崩を起こす。ジッタが無い、あるいは固定のスリープを入れているケースです。復旧直後にまとめて再送が走り、また落ちます。
  • リトライ回数だけを増やしてp99レイテンシが爆発する。成功率の数字は改善しますが、ユーザーは待たされ、上位のタイムアウトに切られます。
  • リトライが上位のタイムアウトの内側に収まっていない。バックオフ待機の合計が全体の締め切りを超えていて、実質1回しか試せていない、あるいは無駄に待ってから切られる。
  • SDKの自動リトライと自前リトライの二重掛け。試行回数が掛け算になり、負荷も待ち時間も想定の数倍になります。
  • 429対策としてAPIキーを増やす。上限の単位が組織やプロジェクトなら効きません。効くかどうかは必ず単位を確認してからにしてください。
  • 429をそのままユーザーに見せて、リロード連打を誘発する。混雑時にリトライを人力で発生させているのと同じです。
  • 失敗したリクエストも上限を消費することを知らずに、間髪入れず再送し続ける。上限の消費だけが進みます。
  • ストリーミングで200を成功と判定してしまい、途中で壊れた応答を正常として扱う。

本記事の仕様・挙動に関する記述は、Anthropic・OpenAI・Googleの公開ドキュメント(2026年8月時点)およびRFC 9110/RFC 6585、AWSアーキテクチャブログ、Google SRE Bookに基づきます。各社のレート制限の具体的な数値、SDKの既定値、サービスティアの提供条件は変更されることがあるため、実装時は必ず最新の一次情報と自組織のコンソール表示を確認してください。

最小構成から始める順番

全部を一度に入れる必要はありません。効果の大きい順に並べると次のようになります。

  1. 1

    エラーを分類してログに残す

    429・5xx・504・クライアントタイムアウトを別々に数え、レート制限ヘッダの値も一緒に残します。ここが無いと、何が起きているか分かりません。
  2. 2

    デッドラインを決める

    機能ごとに「全体で何秒まで待つか」を決め、コードに定数として置きます。リトライ設計はここから逆算します。
  3. 3

    ジッタ付きバックオフとretry-after尊重を入れる

    retry-afterがあれば最優先、無ければFull Jitterのバックオフ。SDKの自動リトライと重ならないよう、どちらか一方に寄せます。
  4. 4

    冪等性を確保する

    業務キーで台帳を持ち、副作用のあるツールを冪等化します。ここまでで、リトライしても壊れない状態になります。
  5. 5

    流量を平準化する

    キュー+ワーカーで同時実行数を自分の手に握り、即時性の要らない処理をバッチAPIへ移します。
  6. 6

    リトライ予算と縮退運転を足す

    全体に対する再試行の比率に上限を設け、閾値を超えたら出力長の縮小・モデルのフォールバック・機能停止へ段階的に落とします。

本番投入前に決めておくこと

  • 機能ごとの全体デッドライン(秒)と、1回あたりのタイムアウト
  • リトライ対象にするエラーの一覧(429・408・5xx・接続エラー)と、対象外にするエラー(400・401・403)
  • retry-afterが無い場合のバックオフ式と上限(cap)、ジッタの入れ方
  • SDKの自動リトライを使うか、自前で持つか。二重掛けにならないこと
  • クライアント全体のリトライ予算(全リクエストに対する再試行の比率)
  • ストリーミングの接続タイムアウト・TTFT・全体タイムアウトの3値
  • 中断したストリームを捨てるか、継続を試みるか
  • 冪等キーの取り方(業務上の一意キー)と、台帳のスキーマ・stale判定時間
  • 副作用のあるツールの冪等化方法
  • 同時実行数の上限(キューのワーカー数)と、優先度の分け方
  • バッチAPIへ移す処理の一覧と、結果取り込みの冪等化
  • 監視する指標としきい値(比率と傾向で置く)、縮退の順番

よくある質問

リトライは何回にすればよいですか
回数から決めないでください。まず全体のデッドライン(この処理は何秒まで待てるか)を決め、その内側に収まる範囲でリトライします。目安としてGoogle SRE Bookはリクエスト単位で3回という上限を紹介していますが、同時にクライアント単位のリトライ予算(リトライ数/総リクエスト数が10%を超えない)を併用することを勧めています。回数だけを増やすとp99レイテンシが悪化し、全体が落ちているときは負荷を増やすだけになります。
429が出たとき、retry-afterヘッダは必ず付いてきますか
必ずではありません。429を定義しているRFC 6585の第4節では、Retry-Afterを含めてもよい(MAY)とされています。実装としては、ヘッダがあればその秒数以上待ち、無ければ自前の指数バックオフ+ジッタにフォールバックする形にしてください。値の形式はRFC 9110の10.2.3でHTTP-dateかdelay-seconds(秒数の整数)のどちらかと定められているので、両方をパースできるようにしておくと安全です。HTTPヘッダ名は大文字小文字を区別しないため、retry-afterとRetry-Afterの表記差は気にしなくて構いません。
429が続くのでAPIキーを増やそうと思います。効果はありますか
上限がかかっている単位を先に確認してください。OpenAIは組織レベルとプロジェクトレベルで定義されユーザ単位ではないと明記しており、Anthropicは組織単位(ワークスペースに組織より低い上限を任意設定可)、Geminiはプロジェクト単位でAPIキー単位ではないとしています。単位が組織やプロジェクトである限り、同じ組織内でキーを増やしても上限は増えません。まずは流量の平準化、バッチAPIへの退避、プロンプトキャッシュの活用を試し、それでも足りないなら上限緩和の手続きを取るのが順当です。
クライアント側でタイムアウトしたリクエストは、再送してよいですか
副作用の有無で判断してください。サーバ側では処理が完了していて課金も発生している可能性があるため、無条件の再送は重複課金と重複副作用のリスクがあります。業務上の一意キーで冪等性台帳を持ち、処理済みなら再実行せず保存済みの結果を返す形にしておけば、少なくともアプリ側の副作用が2回起きることは防げます。メール送信やチケット作成のように外部へ影響するツールは、ツール側も冪等にしておいてください。LLMの推論APIには汎用の冪等性キーが用意されていないことが多く、OpenAIのAPIリファレンス総論にも推論API向けのIdempotency-Keyの記載はありません。
ストリーミングのタイムアウトは何秒にすればよいですか
1つの数字で持たず、接続タイムアウト・初回チャンクまでの時間(TTFT)・全体タイムアウトの3つに分けてください。接続は数秒で十分、TTFTは体感速度に直結するので短め、全体は生成量に応じて長めに取ります。参考として、openai-pythonの既定リクエストタイムアウトは10分で、connect/read/writeを個別指定できます。OpenAIのflex処理のドキュメントでは、応答が遅くなるためクライアントのタイムアウトを15分へ引き上げる例が示されています。Anthropicは10分を超えるような長時間リクエストにはストリーミングかMessage Batches APIの利用を推奨し、アイドル接続の切断に備えてTCP keep-aliveの設定を勧めています。SDKの既定値はバージョンで変わるため、採用時に確認してください。
バッチAPIに移せば、レート制限の問題は解決しますか
同期側の圧力は確実に下がりますが、バッチにもバッチ固有の制限があります。OpenAIのBatch APIは標準のモデル別レート制限とは別のプールで動き、1バッチ最大50,000リクエスト、完了ウィンドウは24時間です。GeminiのBatch APIも非バッチ呼び出しとは別の制限を持ち、同時バッチリクエスト100件やモデルごとのキュー投入トークン上限があります。AnthropicのMessage Batches APIは多くが1時間以内に完了しますが、24時間内に終わらないバッチは期限切れになります(期限切れのリクエストは課金されません)。即時性が要らない処理を移すのは有効で、コストも各社50%程度下がりますが、締め切りのある処理を全部バッチに寄せるのは無理があります。

まとめ

LLM APIの流量制御でつまずく原因は、たいてい「リトライを回数で考えている」ことにあります。回数は結果であって設計変数ではありません。先に決めるのは、全体のデッドライン、再試行の予算、そして冪等性です。この3つが決まっていれば、リトライの実装は20行程度に収まります。

制限のかかり方は1本ではなく、リクエスト数とトークン数など複数の軸が同時に効きます。単位もAPIキーではなく組織やプロジェクトです。具体的な上限値を覚えるより、レスポンスヘッダとコンソールで自分の現在値を読める状態を作るほうが、長く役に立ちます。

そして、リトライを精緻にする前に流量そのものを減らせないかを考えてください。即時性の要らない処理をバッチAPIへ移す、プロンプトキャッシュでトークン制限の消費を減らす、キューとワーカーで送出を平らにならす。この3つは、429の発生そのものを減らします。リトライは最後の受け皿であって、主役ではありません。

うまくいっているかどうかは数字でしか分かりません。429率、リトライ後の最終成功率、p95/p99レイテンシ、デッドライン超過率。この4つが見えていれば、上限緩和を申請すべきか、キューを絞るべきか、モデルを落とすべきかを、感覚ではなく記録で判断できます。

あわせて読みたい

構造化出力(Structured Outputs)とは何か|LLMの返答をプログラムで安全に扱うための仕組み

出典・参考

この記事をシェア

関連する記事

開発・エージェント

LLMアプリのオブザーバビリティ|トレーシングとログ設計で「なぜこの出力になったか」を後から追えるようにする

LLMアプリ・AIエージェントの観測をどう設計するかを実務目線で整理します。通常のWebアプリのログとの違い、最低限とるべきシグナル、OpenTelemetryのGenAI semantic conventions(現在Development段階)の使い方、入出力本文を残すかどうかの判断、コスト可視化とアラート設計、評価(eval)との接続、既存APMとLLM専用ツールの選び分けをまとめます。