APIエラー429 Too Many Requests:レート制限、Retry-After、安全なバックオフ設定

LLM APIにおけるHTTP 429エラー対策ガイド:RPM/TPM制限の仕組み、Retry-Afterヘッダーの正確な読み取り、Full Jitter指数バックオフの実装方法。

HTTP 429 Too Many Requests エラーは、クライアントがAPIプロバイダーのレート制限やトークンクォータを超過した際に発生します。無秩序な即時リトライは問題を解決しないばかりか、リトライストーム(Retry Storm)を引き起こし、アカウントの一時凍結を長期化させる原因になります。

アプリケーションの安定稼働を確保するためには、制限の種類(RPM、TPM、残高不足)を的確に見極め、Retry-After ヘッダーを正しく解析した上で、ランダムな揺らぎを加えた指数バックオフ(Full Jitter Exponential Backoff)を実装する必要があります。

HTTP 429が発生する原因:レート制限の仕組み

最新の大規模言語モデル(LLM)APIでは、主に以下の3つの要因で429ステータスが返されます:

  1. RPM (Requests Per Minute):1分あたりのリクエスト数制限。キューを介さずに多数のスレッドで並列呼び出しを行った際に発生します。
  2. TPM (Tokens Per Minute):1分あたりの合計トークン量(入力+出力)の制限。長いプロンプトや大量の文脈を一度に送信すると到達しやすくなります。
  3. クォータ・残高の枯渇:デポジット残高がゼロになった場合や、設定された利用上限(Spend Limit)に達したことによる制限。
HTTP/1.1 429 Too Many Requests Date: Sun, 23 Aug 2026 03:00:00 GMT Content-Type: application/json Retry-After: 6 x-ratelimit-limit-requests: 500 x-ratelimit-remaining-requests: 0 x-ratelimit-reset-requests: 6s x-ratelimit-limit-tokens: 30000 x-ratelimit-remaining-tokens: 1200 x-ratelimit-reset-tokens: 150ms { "error": { "message": "Rate limit reached for model in organization on tokens per minute (TPM). Please try again in 6s.", "type": "tokens", "param": null, "code": "rate_limit_exceeded" } }

残高不足やサブスクリプションの固定制限が原因である場合、リトライを繰り返してもネットワーク帯域を浪費するだけです。ログの解析に時間をかけずエラー原因を直感的に把握するため、BetterToken では透過的なダッシュボードを提供しています。各リクエストのHTTPステータス、入力・出力・キャッシュトークンの詳細、従量課金残高をリアルタイムで確認でき、突然の5時間利用制限に悩まされる心配がありません。

429エラー診断マトリクス

症状主な原因レスポンス確認項目エンジニアリング対策
並列リクエスト時のエラーRPM制限超過x-ratelimit-remaining-requests: 0セマフォやキューによる並列度の制限
長文プロンプト時のエラーTPM制限超過x-ratelimit-remaining-tokens < プロンプト長文脈の最適化、Prompt Cachingの活用、バッチ分割
すべてのリクエストで429残高またはクォータ枯渇insufficient_quota エラーコードリトライを停止し、残高のチャージや権限を確認
429エラーが雪だるま式に増加リトライストームRetry-After を無視したリトライ待機時間にランダムな揺らぎ(Jitter)を追加

Retry-Afterヘッダーの正確な読み取り方

RFC 6585 では、Retry-After ヘッダーで待機時間を指定する形式として以下の2種類が定義されています:

  • 相対秒数(整数または小数、例:Retry-After: 12
  • HTTP-Date形式の日時(例:Retry-After: Sun, 23 Aug 2026 03:05:00 GMT
import datetime import email.utils import time def parse_retry_after(header_value: str | None, default_delay: float = 1.0) -> float: if not header_value: return default_delay header_value = header_value.strip() try: return max(0.0, float(header_value)) except ValueError: pass try: parsed_date = email.utils.parsedate_to_datetime(header_value) now = datetime.datetime.now(datetime.timezone.utc) delay = (parsed_date - now).total_seconds() return max(0.0, delay) except Exception: return default_delay

Full Jitter 指数バックオフの実装

Retry-After ヘッダーが存在しない場合は、Jitterを加えた指数バックオフ(Full Jitter Exponential Backoff)を使用します。試行回数 ii に対する待機時間計算式は以下の通りです:

Textwait=extrandom(0,min(Textmax,Textbaseimes2i))T_{ ext{wait}} = ext{random}(0, \min(T_{ ext{max}}, T_{ ext{base}} imes 2^i))

import asyncio import json import random import httpx class RateLimitRetryClient: def __init__( self, base_url: str = "https://www.bettertoken.ai/v1", api_key: str = "", max_retries: int = 4, base_delay: float = 1.0, max_delay: float = 32.0, ): self.base_url = base_url self.api_key = api_key self.max_retries = max_retries self.base_delay = base_delay self.max_delay = max_delay self.client = httpx.AsyncClient( base_url=self.base_url, headers={"Authorization": f"Bearer {self.api_key}"}, timeout=60.0, ) async def send_chat_completion(self, payload: dict) -> dict: for attempt in range(self.max_retries + 1): try: response = await self.client.post("/chat/completions", json=payload) if response.status_code == 200: return response.json() if response.status_code == 429: error_data = response.json().get("error", {}) error_code = error_data.get("code") if error_code in ("insufficient_quota", "billing_not_active"): raise RuntimeError(f"請求エラー: {error_data.get('message')}") if attempt == self.max_retries: raise RuntimeError(f"リトライ上限到達 (429): {response.text}") retry_after = response.headers.get("Retry-After") if retry_after: wait_time = parse_retry_after(retry_after) + random.uniform(0.1, 0.5) else: backoff_cap = min(self.max_delay, self.base_delay * (2 ** attempt)) wait_time = random.uniform(0, backoff_cap) await asyncio.sleep(wait_time) continue response.raise_for_status() except httpx.RequestError as exc: if attempt == self.max_retries: raise wait_time = min(self.max_delay, self.base_delay * (2 ** attempt)) await asyncio.sleep(wait_time) raise RuntimeError("リトライ回数の上限に達したためリクエストに失敗しました")

冪等性と安全なリトライ

GET メソッドなどの読み取り操作は安全にリトライできますが、LLM推論などの POST 操作では注意が必要です:

  1. 重複実行の防止:タイムアウトが発生した場合は、再送前にトークンの消費状況や生成状態を確認してください。
  2. リクエストIDの付与X-Request-ID ヘッダーを送信し、ログ上で各試行を一意に追跡できるようにします。
  3. 401/403エラーを429と混同しない:認証エラーは待機しても解消しないため、APIキーの設定を修正する必要があります。

復旧確認とテスト

通常のトラフィックに戻す前に:

  • 最小限のプロンプト(max_tokens: 5)でプローブテストを実行します。
  • HTTP 200が返されること、および x-ratelimit-remaining-requests ヘッダーを確認します。
  • 429エラー率を監視しながら段階的に並列リクエスト数を引き上げます。

厳格な分単位の制限による突然の429エラーを防ぎ、リクエスト状況を完全に可視化するために、BetterToken API へ移行し、専用APIキーを発行してダッシュボードでリアルタイムにトークン消費を管理しましょう。

LLM ワークフローを最適化しませんか?

単一 API でモデルを接続し、キーと AI コストを管理できます。