APIエラー429 Too Many Requests:レート制限、Retry-After、安全なバックオフ設定
LLM APIにおけるHTTP 429エラー対策ガイド:RPM/TPM制限の仕組み、Retry-Afterヘッダーの正確な読み取り、Full Jitter指数バックオフの実装方法。
目次
APIエラー429 Too Many Requests:レート制限、Retry-After、安全なバックオフ設定
HTTP 429 Too Many Requests エラーは、クライアントがAPIプロバイダーのレート制限やトークンクォータを超過した際に発生します。無秩序な即時リトライは問題を解決しないばかりか、リトライストーム(Retry Storm)を引き起こし、アカウントの一時凍結を長期化させる原因になります。
アプリケーションの安定稼働を確保するためには、制限の種類(RPM、TPM、残高不足)を的確に見極め、Retry-After ヘッダーを正しく解析した上で、ランダムな揺らぎを加えた指数バックオフ(Full Jitter Exponential Backoff)を実装する必要があります。
HTTP 429が発生する原因:レート制限の仕組み
最新の大規模言語モデル(LLM)APIでは、主に以下の3つの要因で429ステータスが返されます:
- RPM (Requests Per Minute):1分あたりのリクエスト数制限。キューを介さずに多数のスレッドで並列呼び出しを行った際に発生します。
- TPM (Tokens Per Minute):1分あたりの合計トークン量(入力+出力)の制限。長いプロンプトや大量の文脈を一度に送信すると到達しやすくなります。
- クォータ・残高の枯渇:デポジット残高がゼロになった場合や、設定された利用上限(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)を使用します。試行回数 $i$ に対する待機時間計算式は以下の通りです:
$$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 操作では注意が必要です:
- 重複実行の防止:タイムアウトが発生した場合は、再送前にトークンの消費状況や生成状態を確認してください。
- リクエストIDの付与:
X-Request-IDヘッダーを送信し、ログ上で各試行を一意に追跡できるようにします。 - 401/403エラーを429と混同しない:認証エラーは待機しても解消しないため、APIキーの設定を修正する必要があります。
復旧確認とテスト
通常のトラフィックに戻す前に:
- 最小限のプロンプト(
max_tokens: 5)でプローブテストを実行します。 - HTTP 200が返されること、および
x-ratelimit-remaining-requestsヘッダーを確認します。 - 429エラー率を監視しながら段階的に並列リクエスト数を引き上げます。
厳格な分単位の制限による突然の429エラーを防ぎ、リクエスト状況を完全に可視化するために、BetterToken API へ移行し、専用APIキーを発行してダッシュボードでリアルタイムにトークン消費を管理しましょう。