OpenRouterのレート制限とHTTP 429エラー:原因特定とリクエスト復旧の手順
OpenRouterで発生するHTTP 429(Too Many Requests)および402(Payment Required)エラーの構造と原因を解明する実践ガイド。無料枠の日次クォータ、プラットフォーム全体のレート制限、上流プロバイダの過負荷を切り分ける方法、レスポンスヘッダーや生メタデータ(provider_responses)の調査手順、および標準ライブラリのみを用いた堅牢なPythonリトライスクリプトの設計指針を解説します。
目次

OpenRouterゲートウェイを経由してAIモデルへ大量のリクエストを送信していると、クライアントアプリケーションはしばしば HTTP 429 Too Many Requests ステータスコードに直面します。OpenRouterは数十社におよぶ独立した推論プロバイダを統合しているため、このエラーはインフラストラクチャのまったく異なる階層で発生する可能性があります。原因を確認せずに単純な即時再試行(リトライ)を行うと、クライアント側のアクセス遮断を招いたり、試行回数を無駄に消費したりしがちです。安定したリクエスト送信を回復するには、障害が発生している具体的な階層を特定し、待機時間を設ける(バックオフ)、並行リクエスト数を削減する、モデルのプロバイダを切り替える、あるいは支出制限(上限額)を調整するなど、適切な対処を選択する必要があります。
制限の階層構造:無料枠、プラットフォーム、上流プロバイダ
公式ドキュメントである OpenRouter Limits では、サービス全体のレート制限、エンドプロバイダのスループット、およびアカウント残高による制限を明確に区別しています。
OpenRouter Pricing ページの基本無料プランでは、一般公開されている無料モデル(:free サフィックスが付いたモデル)に対して1日あたり50リクエストの上限が設定されています。分あたりリクエスト数(RPM)の正確な制限値や各利用階層(ティア)のしきい値は変更される可能性があるため、最新の具体的な数値は制限一覧テーブルで確認する必要があります。
エラーを分析する際は、HTTP 429が返される要因となる階層と、関連するHTTP 402ステータスを明確に切り分けることが極めて重要です。
- OpenRouterプラットフォームのレート制限。 ルーター自体に対して極端に高頻度なアクセスが行われた場合に発生します。別個の独立した制限ではなく、このプラットフォームレベルの制限カテゴリに属するのが無料プールの1日あたりのクォータです。無料モデルに対する1日50リクエストの上限を超えると、カウンターがリセットされるまでプラットフォーム側でリクエストが拒否されます。プラットフォーム側のレート制限が発生した場合、サーバーは
X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Resetの各HTTPレスポンスヘッダーを返します。正常な応答であるHTTP 200にはこれらの運用ヘッダーは含まれないため、通常運用時にこれらを参照して制限を事前予測することはできません。 - 上流プロバイダのレート制限(upstream rate limit)。 モデルは特定の企業(Anthropic、Meta、DeepSeek、Mistral、またはサードパーティのホスティング事業者など)のサーバーインフラ上に物理的に展開されています。その提携プロバイダのインフラが過負荷に陥ると、OpenRouterはクライアントに429ステータスをそのまま転送します。レスポンスボディ構造において、
error.metadata.provider_codeフィールドには、プロバイダの名前や識別文字列ではなく、利用可能な場合に上流プロバイダの生の生エラーコード(例: 429)が格納されます。 - 金銭的・残高の制約(
HTTP 402 Payment Required)。 OpenRouter Limits のドキュメントでは、クレジットの枯渇と頻度に関するレート制限を明確に区別しています。402ステータスは、アカウント残高の不足やマイナス残高、あるいは個別APIキーの支出上限(key cap)への到達を示しており、単に組織全体の残高が完全にゼロであることのみを意味するわけではありません。
現在のキーの設定パラメータは、直接APIリクエストを送信して確認できます。
curl -s -X GET https://openrouter.ai/api/v1/key \
-H "Authorization: Bearer $OPENROUTER_API_KEY"
レスポンスペイロードには usage、limit_reset、limit_remaining フィールドが返されます。limit_remaining: null という値は、その特定のAPIキーにローカルな支出上限(key cap)が設定されていないことを示します。この値は組織のメインアカウントに残高が存在することを保証するものではなく、単に対象トークンに対して人為的な支出上限が課されていないことを示しているにすぎません。
診断まとめテーブル
| レスポンスコードと特徴 | 確認ソース | 根本原因 | クライアント側の対処 |
|---|---|---|---|
HTTP 402 Payment Required | エンドポイント GET /api/v1/key または管理ダッシュボード | 組織残高の不足・マイナス、またはAPIキーの支出上限到達(limit_remaining: 0) | 残高のチャージまたはキー上限の引き上げ。設定を変更しない機械的なリトライは無意味 |
HTTP 429(X-RateLimit-* ヘッダーあり) | ゲートウェイのHTTPレスポンスヘッダー | OpenRouterプラットフォーム自体の並行リクエスト数またはリクエスト頻度制限の超過 | Retry-After ヘッダーを確認し、並行スレッド数を削減する |
HTTP 429(provider_code にコードあり) | JSON本文内の error.metadata.provider_code フィールド(任意) | 上流プロバイダの過負荷または停止(上流の生エラーコード) | モデルやプロバイダの切り替えは有効な場合があるが復旧を保証するものではない。Activity > provider_responses で該当プロバイダを特定 |
:free モデルでの HTTP 429 | OpenRouter Pricing ページ | プラットフォームの日次制限(50リクエスト/日)または利用可能キャパシティの枯渇 | 有料モデルへの移行、または実行タスクを後回しにする |
複雑なルーティング時の HTTP 429 | ダッシュボード:Activity > 対象リクエスト > View Raw Metadata | provider_responses オブジェクト内の中間ノードでの障害 | 障害の発生したプロバイダを特定し、BYOK/Routingガイド でフォールバックチェーンを検証 |
2つのシナリオ分析:キー上限到達 vs プロバイダ障害
リクエストが停止した際のクライアントアプリケーションの挙動は、メタデータの精査に基づいて組み立てられます。以下に、2つの想定シナリオ(実アカウントの観察記録ではなく、動作説明のための仮定例)を示します。
シナリオ1(仮定例):個別トークン支出上限の枯渇
この仮定シナリオでは、バックグラウンド処理が HTTP 402 エラーを受け取ります。ここでは前提条件として、アカウント残高自体はプラスであることが管理画面を通じて別途確認されているものとします(APIキーエンドポイントのレスポンス単体では組織全体の残高状態は確認できません)。https://openrouter.ai/api/v1/key エンドポイントへのリクエストは以下を返します。
{
"data": {
"label": "worker-key",
"usage": 25.04,
"limit": 25.0,
"is_free_tier": false,
"limit_remaining": 0.0,
"limit_reset": null
}
}
前提としてアカウント全体の残高はプラスですが、limit_remaining フィールドはゼロになっています。このトークンは、管理者が設定した25ドルの支出上限に到達しました。このキーを使用して何度リトライを実行しても、同じ402エラーが返されるだけです。ワーカープロセスは即座に処理を中断し、管理者に上限引き上げを促すアラートを通知する必要があります。
シナリオ2(仮定例):上流プロバイダの過負荷
説明のための事例として、上流側の障害によってリクエストが HTTP 429 を返した場合を考えます。429ステータスを受信したという事実だけでは、ゲートウェイ全体の稼働状況やアカウント残高の有無を判断することはできません。エラーレスポンスの本文には、以下のようなメタデータブロックが含まれる場合があります。
{
"error": {
"message": "Provider returned rate limit error",
"code": 429,
"metadata": {
"provider_code": 429
}
}
}
error.metadata.provider_code フィールドは任意(オプショナル)であり、提供されている場合は上流プロバイダの生のステータスコード(この例では429)を示します。プロバイダの名前や文字列識別子ではありません。このコードの存在だけでは、具体的にどのホストがリクエストを拒否したのかを特定することはできません。
障害を起こしたプロバイダを特定するには、管理コンソールで Activity > 対象リクエスト > View Raw Metadata を開く必要があります。ルーティングガイド で解説されているように、provider_responses オブジェクトには問い合わせが行われた各ホストとその実際のステータスが表示されます。このような状況では、別のプロバイダへの切り替えやモデルの変更が問題解決に役立つ場合もありますが、即座の復旧が保証されるわけではありません。
Python 3によるクライアント側リトライスクリプト
HTTP 429ステータスが一過性のものである場合、次回試行までの待機時間は Retry-After ヘッダーに基づいて算出します。このヘッダーは、秒単位の整数またはHTTP形式の日時文字列としてサーバーから渡されます。
以下の実装はPython 3の標準ライブラリのみを使用しています。HTTP 429エラーのみを対象にリトライを行い、サーバーから待機ヘッダーが提供されない場合はランダムなジッター(ゆらぎ)を加えた指数バックオフを適用し、サーバーから要求された待機時間が60秒を超える場合は実行を中止します。
import email.utils
import json
import os
import random
import sys
import time
import urllib.error
import urllib.request
API_KEY = os.environ.get("OPENROUTER_API_KEY")
MODEL_ID = os.environ.get("OPENROUTER_MODEL_ID", "openai/gpt-4o-mini")
MAX_ATTEMPTS = 3
MAX_ACCEPTABLE_WAIT = 60.0
def parse_retry_after(header_value: str | None) -> float | None:
if not header_value:
return None
raw = header_value.strip()
if raw.isdigit():
return max(0.0, float(raw))
try:
parsed_date = email.utils.parsedate_to_datetime(raw)
delay = parsed_date.timestamp() - time.time()
return max(0.0, delay)
except Exception:
return None
def execute_completion(prompt_text: str) -> str | None:
if not API_KEY:
sys.stderr.write("Переменная окружения OPENROUTER_API_KEY не задана.\n")
return None
endpoint = "https://openrouter.ai/api/v1/chat/completions"
payload = json.dumps(
{
"model": MODEL_ID,
"messages": [{"role": "user", "content": prompt_text}],
}
).encode("utf-8")
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
for attempt in range(1, MAX_ATTEMPTS + 1):
req = urllib.request.Request(endpoint, data=payload, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as response:
status_code = response.getcode()
body = response.read().decode("utf-8")
if status_code == 200:
data = json.loads(body)
return data["choices"][0]["message"]["content"]
except urllib.error.HTTPError as err:
if err.code == 429:
retry_header = err.headers.get("Retry-After")
server_delay = parse_retry_after(retry_header)
if server_delay is not None:
wait_seconds = server_delay
else:
base_delay = 2.0 ** attempt
wait_seconds = base_delay + random.uniform(0.1, 1.0)
if wait_seconds > MAX_ACCEPTABLE_WAIT:
sys.stderr.write(
f"Сервер запросил паузу {wait_seconds:.1f} с. "
"Ожидание превышает 60 секунд. Запрос отменен.\n"
)
return None
if attempt == MAX_ATTEMPTS:
sys.stderr.write("Исчерпан лимит из 3 попыток на статус 429.\n")
return None
sys.stderr.write(
f"Получен 429. Попытка {attempt} завершилась неудачей. "
f"Пауза {wait_seconds:.2f} с перед следующим запросом.\n"
)
time.sleep(wait_seconds)
continue
elif err.code == 402:
sys.stderr.write("Ошибка 402: проверьте баланс счета и лимит ключа.\n")
return None
else:
sys.stderr.write(f"HTTP-ошибка {err.code}: запрос отклонен без повтора.\n")
return None
except urllib.error.URLError as err:
sys.stderr.write(f"Сетевой сбой: {err.reason}. Повтор отменен.\n")
return None
return None
if __name__ == "__main__":
result = execute_completion("Назови три базовых принципа надежности сетевых API.")
if result:
print(result)
上記スクリプトは元の診断ログ文字列(ロシア語)をバイト単位でそのまま保持しているため、内部の判定分岐(ディシジョンツリー)と対応する日本語の意味を以下に整理します。
Переменная окружения OPENROUTER_API_KEY не задана.(環境変数 OPENROUTER_API_KEY が設定されていません):環境変数が未設定であることを検知し、ネットワーク呼び出しを行わずに直ちに処理を終了します。Сервер запросил паузу {wait_seconds:.1f} с. Ожидание превышает 60 секунд. Запрос отменен.(サーバーから {wait_seconds:.1f} 秒の待機が要求されました。待機時間が60秒を超えるため、リクエストを中止します):サーバーのRetry-Afterヘッダーで指定された待機時間がMAX_ACCEPTABLE_WAIT(60秒)を超過した場合に発火し、ワーカープロセスが長時間ブロックされるのを防ぐため即座に処理を中止します。Исчерпан лимит из 3 попыток на статус 429.(ステータス429に対する3回のリトライ上限に達しました):HTTP 429に対するリトライ試行回数が上限の3回すべて失敗したことを示します。Получен 429. Попытка {attempt} завершилась неудачей. Пауза {wait_seconds:.2f} с перед следующим запросом.(429を受信しました。試行 {attempt} が失敗しました。次のリクエストまで {wait_seconds:.2f} 秒待機します):試行途中でレート制限に遭遇したログを記録し、計算されたバックオフ時間だけスリープして再試行に備えます。Ошибка 402: проверьте баланс счета и лимит ключа.(エラー402:アカウント残高およびキーの上限を確認してください):アカウント残高の枯渇やキーの支出上限到達を示す回復不能なHTTP 402エラーを記録し、再試行を行わずに即時終了します。HTTP-ошибка {err.code}: запрос отклонен без повтора.(HTTPエラー {err.code}:リトライせずにリクエストを拒否しました):429および402以外のHTTPエラーステータスコードを記録し、再試行を行わずに終了します。Сетевой сбой: {err.reason}. Повтор отменен.(ネットワーク障害:{err.reason}。リトライを中止しました):ソケット切断やタイムアウトなどのネットワークエラー(URLError)を捕捉し、前回の要求がサーバーに届いて処理されたかどうかが不明な状態での重複実行を防ぐために処理を中止します。- テスト用プロンプト
Назови три базовых принципа надежности сетевых API.は「ネットワークAPIの信頼性に関する3つの基本原則を挙げてください」という意味です。
ネットワークリクエストの再試行と副作用
自律型エージェントのループ内でツール呼び出し(外部関数の実行)を伴うワークフローを設計する場合、HTTPリクエストのリトライには細心の注意が必要です。前段のステップでモデルがすでに外部状態を変更するツール(データベースへの書き込み、決済処理の実行、サポートチケットの発行など)を呼び出していた場合、一連の処理を安易に最初から再実行すると、同一操作が重複して実行されるリスクが生じます。関連するビジネスツールがすでに実行されている場合や、通信障害やソケットタイムアウトのようにリクエストが実際にサーバー側で受理・処理されたかどうかが曖昧なネットワーク結果となった場合には、自動リトライを行ってはなりません。
上記のスクリプトでは、モデルによって明示的に拒否されたHTTP 429ステータスコードの場合にのみ厳格にリトライを限定しています。重要な点として、テキスト生成は決して厳密に冪等(idempotent)でも無料でもないという点に留意する必要があります。再試行によって追加のトークン枠や予算が消費されるだけでなく、モデル生成の確率論的な性質により、後続の生成結果が初回と異なる出力を返す可能性があります。エージェントが外部コマンドを実行している最中に障害が発生した場合は、モデルとの対話を再開する前に行動ログ(アクションログ)を通じてシステムの状態を同期・整合させる必要があります。
耐障害性の高いルーティング設計
料金プランページにおいて、Freeプランでは無料モデル全体に対してプラットフォームレベルで1日50リクエストの上限が設定されています。公式ドキュメントで言及されているプラットフォーム制限の撤廃は、有料モデルへの切り替えを指しており、単にアカウントにクレジットをチャージすることを意味しているわけではありません。残高をチャージしたとしても、:free エンドポイントに課された制限が自動的に解除されることはありません。実際のアカウント条件やクォータの変動については、常に制限一覧テーブルを参照して確認してください。さらに、有料モデルを利用している場合であっても、個別のプロバイダサーバー群の過負荷を完全に排除できるわけではありません(プロバイダの切り替えは問題緩和に役立ちますが、即時の復旧を保証するものではありません)。
本番環境でシステムの信頼性を確保するため、エンジニアリングチームは一般的に以下の防御策を組み合わせて運用しています。
- OpenRouterリクエストの
modelsパラメータ配列に代替モデル(フォールバックモデル)を指定し、プライマリのモデル提供者が障害を起こした際にルーターが予備のプロバイダへ自動的にリクエストを迂回できるようにする。 - タスクキュー、レートリミッター、またはトークンバケット機構を用いて、クライアント側で同時並行リクエスト数の上限を厳格に制御する。
- 極めて重要な基幹インフラにおいては、互換性のあるリクエストスキーマを持つ他のマルチモデルAPIを経由する独立したバックアップルートを維持し、プライマリゲートウェイの長時間にわたる停止時にもトラフィックを迅速に移行できるように備える。