APIエラーとリトライの基本
401・429・5xxを切り分け、SDKの自動再試行に任せる範囲を決める

APIエラーとリトライの基本401・429・5xxを切り分け、SDKの自動再試行に任せる範囲を決める

前回の記事では、response.usage からトークン数を取り出して料金を概算しました。ログに「成功/失敗」を残しておこう、と書いたところで終わっています。今回はその「失敗」のほうを扱います。

APIを繰り返し使い始めると、必ずこうなります。

401  認証エラー
429  レート制限・残高不足
5xx  サーバー側のエラー
     タイムアウト

このとき、いちばんやってはいけないのが「エラーが出たから、もう一度送る」を全部のエラーに対してやることです。APIキーが間違っているなら、100回送っても101回目も失敗します。そのあいだ、失敗したリクエストもレート制限にカウントされていきます。

先に、この記事の結論を3つ書いておきます。

  • 再試行そのものは、すでにSDKがやっています。 自分で書き直す前に、何をどう再試行しているかを知るのが先です
  • それでも自分で書く部分があります。 「待っても直らないエラーを、待たずにやめる」判断です
  • 上限は回数ではなく時間で決めます。 回数で決めると、SDKの再試行と掛け算になって読めなくなります

この記事を読み終えると、次のことができるようになります。

  • 401と429の違いを説明できる
  • 再試行してよいエラーと、してはいけないエラーを分けられる
  • SDKが既定で何をどう再試行しているかを説明できる
  • timeoutmax_retries を意図をもって設定できる
  • 同じ429でも「待っても直らない4種類」を見分けられる
  • 再試行に締切(デッドライン)を設けられる
  • 二重リトライを避けられる
  • 本物のエラーを待たずに、再試行の動作を手元で確かめられる
  • request ID をログへ残せる
Screenshot

【月1 特定テーマ講座(11月)】
POS売上分析Copilotを作る
(POSデータ×PYTHON×生成AI)

【開催日時】 全2回(土)2026/11/7,11/21(13:30〜18:00)
【受講形式】 当日Zoom( or 復習用に後日動画視聴)
【参加費用】 2万2千円(税込み)/人

先に、この記事で出てくる用語を整理します

エラーの話は、用語が分からないだけで急に難しく感じます。この記事で使う言葉を先にまとめておきます。いま全部覚える必要はありません。 本文中でも初出のたびに説明しますので、分からなくなったらここへ戻ってきてください。

用語 この記事での意味
HTTPステータス 401、429、503 のような3桁の数字。おおまかな失敗の種類を表します
error code ステータスより細かい、文字列の識別子。slow_down のような形です
SDK OpenAIが配布しているPythonのライブラリ。pip install openai で入るもの
再試行(リトライ) 失敗したリクエストを、もう一度送ること
指数バックオフ 失敗するほど待ち時間を倍にしていく方法。1秒→2秒→4秒→8秒
ジッター 待ち時間に加える短いランダムのゆらぎ。複数の処理が同時刻に再開しないようにするもの
Retry-After 「最低これだけ待ってから来てください」とサーバーが返すHTTPヘッダー
レート制限 一定時間内に送れるリクエスト数・トークン数の上限
タイムアウト 「何秒待っても返ってこなければ諦める」という設定
デッドライン 再試行を含めて「何秒までなら粘ってよいか」という締切
冪等(べきとう) 同じ操作を2回やっても結果が1回分と同じであること
request ID リクエスト1件ごとに振られる識別子。問い合わせや調査に使います

エラーは「待てば直るか」で2つに分ける

エラーコードを全部覚える必要はありません。最初の判断は1つだけです。

これは、待てば直る可能性があるか。それとも、こちらが何かを直さない限り直らないか。

APIエラーを再試行してよいか、原因を直すかの切り分け待てば直る可能性があるのは接続エラー、408、409、送りすぎによる429、5xx。直さない限り直らないのは401、403、404、400、そして残高や上限による429。429は両方に出てくるため、HTTPステータスだけでは判断できない。エラーは「待てば直るか」で2つに分けるAPIがエラーを返した待てば直る可能性がある→ 少し待って、もう一度送る接続エラーAPIConnectionError408APITimeoutError409ConflictError429(送りすぎ)RateLimitError5xxInternalServerError直さない限り直らない→ 原因を直してから送る401AuthenticationError403PermissionDeniedError404NotFoundError400BadRequestError429(残高・上限)RateLimitError429 は両方に出てくる。ステータスだけでは決まらない

 待てば直る可能性があるもの

状況 ステータス SDKの例外クラス
ネットワークがつながらなかった (なし) APIConnectionError
時間内に返ってこなかった 408 APITimeoutError
一時的な競合 409 ConflictError
レート制限に当たった 429 RateLimitError
サーバー側のエラー・モデルの過負荷 5xx InternalServerError

一時的な混雑やネットワークの問題なら、少し待ってからやり直すと成功する可能性があります。

503 も InternalServerError です。 SDKには ServiceUnavailableError のようなクラスはなく、500以上はすべて InternalServerError にまとめられます。503を個別に扱いたい場合は、例外の status_code を見てください。

 直さない限り直らないもの

状況 ステータス SDKの例外クラス
APIキーが違う・読めていない 401 AuthenticationError
権限がない・地域が非対応 403 PermissionDeniedError
モデル名やIDが間違っている 404 NotFoundError
リクエストの中身が不正 400 BadRequestError
残高不足・利用上限に到達 429 RateLimitError

最後の行に注目してください。429は両方の表に出てきます。 ここがこの記事のいちばん細かい話になるので、後でまた戻ってきます。

 401は、何回送っても401

いちばん分かりやすい例が401です。

401 が返った
  ↓ 1秒待つ
もう一度、同じリクエストを送る
  ↓
また 401
  ↓ 2秒待つ
また送る … 以下同じ

APIキーが間違っているなら、待っても変わりません。確認するのは次の4点です。

  • OPENAI_API_KEY が設定されているか
  • そのキーが無効化されていないか
  • 別のプロジェクトのキーを読んでいないか
  • キーに必要な権限が付いているか

環境変数の設定については、以前の記事で扱っています。

SDKは、すでに再試行している

ここが、この記事でいちばん先に知っておいてほしいところです。

自分で再試行を書き始める前に、SDKがすでに何をしているかを確認してください。

次のコードには、再試行の記述が1行もありません。

from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-5.6-luna",
    input="先週の売上を2文以内で要約してください。",
)

それでも、一時的なエラーはSDKの内部で自動的に再試行されています。

OpenAI Python SDKの自動再試行の動きSDKは既定で2回再試行するため、最初の1回と合わせて最大3リクエストが飛ぶ。待ち時間は0.5秒から2倍ずつ増え、1.0秒、2.0秒、4.0秒、8.0秒で頭打ちになり、そこから0〜25パーセント短くしたジッターが入る。Retry-Afterヘッダーがあればこの計算より優先され、120秒を超える指定なら再試行せずに諦める。SDKは、すでに再試行している(既定 max_retries = 2)コードに1行も書かなくても、内部でこう動く送信 1失敗送信 2失敗送信 3成功0.5秒 待つ1.0秒 待つ1回目 + 再試行2回 = 最大3リクエスト待ち時間 = 0.5秒 × 2の累乗(上限8秒)、そこから0〜25%短くする0.5秒1回目の後1.0秒2回目の後2.0秒3回目の後4.0秒4回目の後8.0秒5回目の後ここで頭打ちRetry-After ヘッダーがあれば、この計算より優先される(120秒を超える指定なら、再試行せずに諦める)

 既定で2回、何を再試行するか

SDKが自動で再試行するのは次の5種類です。これ以外は再試行されず、そのまま例外として上がってきます。

接続エラー(ネットワーク到達不能など)
408  Request Timeout
409  Conflict
429  Rate Limit
5xx  サーバー側のエラー

回数は既定で 2回 です。つまり最初の1回と合わせて、最大3リクエストが飛びます。

 待ち時間は0.5秒から、上限8秒

待ち方も決まっています。0.5秒 × 2の累乗で増えていき、8秒で頭打ちになります。

何回目の失敗の後か 基準の待ち時間 実際の待ち時間(ジッター込み)
1回目 0.5秒 0.38〜0.50秒
2回目 1.0秒 0.75〜1.00秒
3回目 2.0秒 1.50〜2.00秒
4回目 4.0秒 3.00〜4.00秒
5回目以降 8.0秒(上限) 6.00〜8.00秒

ジッターは、待ち時間に加えるランダムなゆらぎのことです。複数の処理が同時に失敗したとき、全員が同じ秒数だけ待つと、また同時に再開してまた混雑します。それを避けるためのものです。SDKでは基準の待ち時間を0〜25%だけ短くする形で入っています。

 Retry-After があれば、そちらに従う

ここが重要です。 レート制限(429)やモデルの過負荷(503)のレスポンスには、Retry-After というHTTPヘッダーが付くことがあります。

Retry-After: 56

これは「少なくとも56秒待ってから再試行してください」という、サーバー側からの指定です。公式ガイドでも、このヘッダーがある場合はその指定を最小の待機時間として扱うよう案内されています。

SDKは、このヘッダーを読んで従います。 しかも、かなり丁寧に処理しています。

  • 秒数(Retry-After: 56)だけでなく、ミリ秒(retry-after-ms)とHTTP日付形式も解釈します
  • ヘッダーがあれば、自前の指数バックオフより優先します
  • 指定が120秒を超えていたら、再試行せずに諦めます(長すぎる待機でプロセスを止めないため)
  • x-should-retry というヘッダーでサーバーが明示的に指示していれば、それを最優先します

ここから分かることがあります。SDKの自動再試行を止めて自分で指数バックオフを書くと、この Retry-After への対応をいったん捨てることになります。 書き直すなら、同じ処理を自分で実装し直す覚悟が要ります。

それでも自前のラッパーが要る理由

ではSDKに全部任せればよいかというと、1点だけ足りません。

 「待っても直らない429」が4種類ある

429は「送りすぎ」だけを意味しません。公式のエラーコード一覧を見ると、429には複数の原因があります。

429エラーの6種類のerror codeと、再試行してよいかどうか429には6種類のerror codeがある。レート制限とslow_downは待てば直る。credit_balance_exhausted、organization_spend_limit_exceeded、project_spend_limit_exceeded、organization_usage_limit_exceededの4つは、支払いや上限設定を変えない限り待っても直らない。SDKの自動再試行はHTTPステータスしか見ないため、この4つも再試行してしまう。同じ 429 でも、待っても直らないものが4種類ある429Rate Limit(レート制限)短時間に送りすぎた待てば直るslow_down増やし方が急すぎた待てば直るcredit_balance_exhausted前払い残高がない待っても直らないorganization_spend_limit_exceeded組織の支出上限に到達待っても直らないproject_spend_limit_exceededプロジェクトの支出上限に到達待っても直らないorganization_usage_limit_exceeded組織の月次利用上限に到達待っても直らないSDKの自動再試行はステータスしか見ない。この4つも3回送ってしまう
error code 意味 待てば直る?
(レート制限) 短時間に送りすぎた
slow_down リクエストの増やし方が急すぎた
credit_balance_exhausted 前払いクレジットの残高がない ×
organization_spend_limit_exceeded 組織の支出上限に到達 ×
project_spend_limit_exceeded プロジェクトの月次支出上限に到達 ×
organization_usage_limit_exceeded 組織の月次利用上限に到達 ×

下の4つは、支払いや上限設定を変えない限り、何秒待っても直りません

 SDKは、この4つも再試行してしまう

SDKの判定はHTTPステータスだけを見ています。429ならすべて再試行対象です。error code までは見ていません。

つまり、残高が尽きた状態でAPIを呼ぶと、こうなります。

1回目 → 429 credit_balance_exhausted
  0.5秒待つ
2回目 → 429 credit_balance_exhausted
  1.0秒待つ
3回目 → 429 credit_balance_exhausted
  例外が上がる

3リクエスト・約1.5秒を、確実に失敗すると分かっている処理に使った

1回なら1.5秒です。しかし100店舗ぶんのバッチで全件がこれになると、300リクエスト・2分半が無駄になります。しかも失敗したリクエストもレート制限にカウントされます。

これが、自前のラッパーを書く理由です。 指数バックオフを書き直すためではありません。SDKが見ていない error code を見て、待たずにやめるためです。

 429になる前に気づく方法もある

ついでに触れておきます。レスポンスのヘッダーには、レート制限の残りが入っています。

x-ratelimit-limit-requests       1分あたりのリクエスト上限
x-ratelimit-remaining-requests   残りのリクエスト数
x-ratelimit-reset-requests       上限が回復するまでの時間
x-ratelimit-limit-tokens         1分あたりのトークン上限
x-ratelimit-remaining-tokens     残りのトークン数
x-ratelimit-reset-tokens         上限が回復するまでの時間

429になってから対処するのではなく、残りが減ってきたら送信の間隔を空けるという設計もできます。大量に流すバッチでは、こちらのほうが効きます。

まず timeout を決める

ラッパーを書く前に、timeout を設定します。

 既定は10分

SDKの既定のタイムアウトは600秒(10分)です。接続だけは5秒で切られますが、応答を待つ時間は10分あります。

短い分析コメントを1つ作るだけの処理で、10分待つ設計にはしないはずです。

from openai import OpenAI

client = OpenAI(
    timeout=30.0,          # 1回のリクエストを30秒で打ち切る
)                          # max_retries は既定の 2 のまま

時間内に返ってこなければ APITimeoutError が発生します。

 タイムアウトした分も再試行される

注意点が1つあります。timeout=30.0 にしても、処理全体が30秒で終わるとは限りません。

タイムアウトも再試行の対象なので、SDKの既定(2回)が効いていれば、最悪の場合は次のようになります。

30秒(1回目)+ 0.5秒 + 30秒(2回目)+ 1秒 + 30秒(3回目)
= 約91.5秒

1回あたりの上限と、処理全体の上限は別物です。 後で扱うデッドラインは、この「全体」のほうを抑えるものです。

APITimeoutErrorAPIConnectionErrorサブクラスです。except openai.APIConnectionError と書くと、タイムアウトもそこで捕まります。両方を別々の except に並べると、後ろに書いたほうは実行されないので注意してください。

待っても直らない429を止める

いよいよラッパーを書きます。やることは1つだけです。「待っても直らない429なら、待たずに諦める」。

 4つのコードを定数にする

import time
import openai
from openai import OpenAI

# 待っても直らない429(公式のエラーコード一覧より)
待っても直らない = {
    "credit_balance_exhausted",
    "organization_spend_limit_exceeded",
    "project_spend_limit_exceeded",
    "organization_usage_limit_exceeded",
}

client = OpenAI(timeout=30.0)      # max_retries は既定の 2 のまま

集合(set)にしているのは、in で確認するためです。error code が増えたときも、ここへ1行足すだけで済みます。

 ラッパーを書く

def 呼ぶ(入力, 制限秒=60.0):
    期限 = time.monotonic() + 制限秒
    待ち = 2.0

    while True:
        try:
            return client.responses.create(
                model="gpt-5.6-luna",
                input=入力,
            )

        # 「残高不足」や「上限超過」などのエラーコードに該当する場合
        except openai.RateLimitError as exc:
            if exc.code in 待っても直らない:
                raise # リトライせず即座に例外を発生
            直前のエラー = exc

        # 通信遮断やサーバー側エラーの場合(リトライ対象)
        except (openai.APIConnectionError,
                openai.InternalServerError) as exc:
            直前のエラー = exc

        # 残りの猶予時間を計算
        残り = 期限 - time.monotonic()

        if 残り <= 待ち:
            raise 直前のエラー             # 締切までに次の再試行が収まらない

        time.sleep(待ち)
        待ち = min(待ち * 2, 30.0)
  • exc.code で error code が取れます。 SDKはレスポンスの error.code をここへ入れてくれます。exc.status_code(429)だけでは、この4つを見分けられません
  • 401や400の except は書いていません。 捕まえないものは自動的に抜けていきます。except を、書いても書かなくても動作が同じです
  • 直前のエラー = exc と代入しているのは、except ... as excexc がブロックを抜けた時点で消えるからです。ブロックの外で raise し直すには、別の名前へ移しておく必要があります
  • 待ち時間の計算が1箇所にまとまっています。 429の分岐と5xxの分岐で同じコードを2回書くと、片方だけ直して食い違う原因になります

回数ではなく、締切で止める

 最大試行回数だけでは足りない

「最大4回まで」と決めても、実際に何秒かかるかは決まりません。1回のリクエストに30秒かかるかもしれないからです。

業務側の要件は、たいてい回数ではなく時間で来ます。

  • 画面で人が待っている → 30秒まで
  • 夜間バッチ → 5分まで

だから上のコードでは、回数ではなく 制限秒 を引数にしています。

再試行の上限を回数ではなく時間で決めた場合の送信回数初回2秒・上限30秒のバックオフで締切を設けると、10秒なら送信3回で待機の累計6秒、30秒なら4回で14秒、60秒なら5回で30秒、300秒なら13回で270秒になる。締切を決めれば、何回にするかを決める必要がなくなる。回数ではなく、締切で止める初回2秒・上限30秒でバックオフした場合0秒60秒120秒180秒240秒300秒10秒待機の累計 6秒 / 送信 3 回画面で人が待つ30秒待機の累計 14秒 / 送信 4 回対話型の上限60秒待機の累計 30秒 / 送信 5 回1件ずつの定期処理300秒待機の累計 270秒 / 送信 13 回夜間バッチ締切を決めれば、「何回にするか」を考えなくてよくなる

 デッドラインを渡す

time.monotonic() は、途中で時刻が変更されても巻き戻らない時計です。経過時間を測るときは、time.time() ではなくこちらを使います。

締切を変えると、送信回数は自動的に決まります。

制限秒 API送信の回数 待機の累計 向いている用途
10秒 3回 6秒 画面で人が待っている
30秒 4回 14秒 対話型の上限
60秒 5回 30秒 1件ずつの定期処理
300秒 13回 270秒 夜間バッチ

初回の待ちを2秒、上限を30秒とした場合の値です。「何回にすればよいか」を考えなくてよくなるのが、この書き方の利点です。

二重リトライに気をつける

 送信回数は掛け算になる

ここで、上のコードに1つ落とし穴があります。SDKの自動再試行を既定のまま残しているので、自前のループの1周が「1リクエスト」とは限りません。

SDKの自動再試行とアプリ側の再試行が掛け算になることSDKのmax_retriesが既定の2なら1周あたり3リクエスト。アプリ側で5回ループすると最大15回のAPI送信になる。アプリ側3回なら9回、max_retriesを0にすれば3回、max_retriesを5にしてアプリ側5回なら30回。回数で管理すると掛け算になるが、締切で管理すれば全体の時間は超えない。SDKとアプリの再試行は、足し算ではなく掛け算SDK の max_retries1周あたりの送信アプリ側のループ最大の API 送信回数23 リクエスト×5 回=15 回既定のまま23 リクエスト×3 回=9 回01 リクエスト×3 回=3 回56 リクエスト×5 回=30 回回数で管理すると掛け算になる。締切で管理すれば、掛かっても時間は超えない
SDKの max_retries 1周あたりの送信 自前のループ 最大のAPI送信回数
2(既定) 3 5回 15回
2(既定) 3 3回 9回
0 1 3回 3回
5 6 5回 30回

公式のレート制限ガイドでも、アプリ側で再試行を管理するならSDKの再試行を無効にするか、SDK側も含めて上限を管理するよう案内されています。

 どちらかに寄せる

対処は2つあります。

やり方 設定 向いている場面
SDKに再試行を任せる max_retries は既定のまま。自前のループはデッドラインで止める Retry-After への対応をSDKに任せられる。ほとんどの場合こちら
自前に寄せる max_retries=0。待ち方もすべて自分で書く 待ち方を完全に制御したい場合。Retry-After の処理も自分で書く必要がある

この記事では前者を採っています。 回数ではなく締切で止めているので、SDKの再試行が何回入っても全体の時間は締切を超えません。掛け算が怖いのは「回数で管理しているとき」です。

max_retries=0 にする場合は、Retry-After の読み取りを忘れないでください。exc.response.headers.get("retry-after") で取れます。これを実装しないと、サーバーが「56秒待って」と言っているのに2秒で再送することになり、かえって状況を悪くします。

擬似的に429を起こして確かめる

ここまで書いたコードが本当に意図どおり動くか、本物のエラーを待たずに確かめます。

 偽のクライアントを作る

やり方は単純です。決めた順番で例外を投げるだけの、偽の client を作って差し替えます。

import httpx2
import openai


def 偽エラー(status, code=None):
    """本物と同じ形の例外オブジェクトを作る"""
    req = httpx2.Request("POST", "https://api.openai.com/v1/responses")
    body = {"error": {"message": "test", "type": "x", "code": code}}
    res = httpx2.Response(
        status,
        headers={"x-request-id": "req_test"},
        json=body,
        request=req,
    )
    return openai.OpenAI(api_key="dummy")._make_status_error(
        "test", body=body, response=res
    )


class 偽クライアント:
    """決めた順に例外を投げる。None なら成功を返す"""

    def __init__(self, 例外列):
        self.例外列 = list(例外列)
        self.回数 = 0
        self.responses = self

    def create(self, **kwargs):
        self.回数 += 1
        e = self.例外列.pop(0) if self.例外列 else None
        if e is None:
            class R:
                output_text = "成功しました"
            return R()
        raise e

httpx2 はSDKが内部で使っているHTTPライブラリで、openai を入れると一緒に入ります。_make_status_errorSDKがレスポンスから例外を作るときに通る関数そのものなので、本物とまったく同じクラス・同じ code の例外が手に入ります。

 6パターンを流す

ケース = [
    ("429 レート制限 → 2回目で成功", [偽エラー(429, "rate_limit_exceeded"), None]),
    ("429 slow_down → 3回目で成功", [偽エラー(429, "slow_down")] * 2 + [None]),
    ("429 残高不足", [偽エラー(429, "credit_balance_exhausted")] * 9),
    ("429 支出上限", [偽エラー(429, "organization_spend_limit_exceeded")] * 9),
    ("503 過負荷が続く", [偽エラー(503, "server_is_overloaded")] * 9),
    ("401 認証エラー", [偽エラー(401)] * 9),
]

for 名前, 列 in ケース:
    c = 偽クライアント(列)
    try:
        呼ぶ(c, "テスト", 制限秒=10.0)
        結果 = "成功"
    except Exception as exc:
        結果 = type(exc).__name__
    print(f"{名前}  →  {結果}(送信 {c.回数} 回)")

呼ぶ() の第1引数に client を受け取るよう、少しだけ書き換えて実行します。結果はこうなります。

429 レート制限 → 2回目で成功  →  成功(送信 2 回)
429 slow_down → 3回目で成功   →  成功(送信 3 回)
429 残高不足                  →  RateLimitError(送信 1 回)
429 支出上限                  →  RateLimitError(送信 1 回)
503 過負荷が続く              →  InternalServerError(送信 6 回)
401 認証エラー                →  AuthenticationError(送信 1 回)

見るべきは3行目と4行目の「送信 1 回」です。同じ429でも、待っても直らないと分かっているものは1回でやめています。これが狙いどおり動いている証拠です。

そして6行目の401も「送信 1 回」です。except を1つも書いていないのに、再試行されずに上へ抜けています。

request ID をログへ残す

 成功時とエラー時

エラーを調べるとき、いちばん役に立つのが request ID です。リクエスト1件ごとに振られる識別子で、レスポンスの x-request-id ヘッダーに入っています。

# 成功したとき
print(response._request_id)          # req_abc123...

# エラーになったとき
try:
    response = client.responses.create(...)
except openai.APIStatusError as exc:
    print("request_id:", exc.request_id)
    print("status:", exc.status_code)
    print("code:", exc.code)
    raise

APIStatusError4xx・5xxの例外すべての親クラスです。これ1つを捕まえれば、401でも429でも503でも request_id が取れます。

成功時のほうは response._request_id とアンダースコアが付いています。読みにくいですが、これが公式ドキュメントに載っている書き方です。APIConnectionError(接続できなかった場合)には request_id がありません。 リクエストがサーバーへ届いていないので、IDも振られていないためです。

 残す項目と、残してはいけない項目

残す 何に使うか
日時、処理名、モデル いつ・どこで起きたかの特定
試行回数、待機した秒数 再試行が効いていたかの確認
例外クラス名、HTTPステータス、error code 再試行してよかったのかの検証
request ID サポートへの問い合わせ、個別調査
最終的に成功したか失敗したか 再実行の要否
残さない 理由
APIキー ログは共有されます。キーが漏れる最も多い経路の1つです
個人情報 ログの保管期間と、個人情報の保管ルールは別物です
送信したデータの全文 必要なら件数やハッシュだけを残します

前回の記事でトークン数と概算料金をログへ残しました。そこへ「試行回数」と「成功/失敗」を足すと、「今月APIの請求が増えたのは、リトライが増えたからではないか」まで後から追えるようになります。

エラーの切り分け表

最初の判断に使う表です。手元に置いておいてください。

状況 ステータス 例外クラス 同じ内容を再試行? 最初にすること
接続できない APIConnectionError 待って再試行。ネットワークも確認
時間内に返らない 408 APITimeoutError timeout の値を見直す
競合 409 ConflictError 待って再試行
レート制限 429 RateLimitError 条件次第 exc.codeRetry-After を見る
残高・上限 429 RateLimitError × 支払い設定・利用上限を確認
サーバー障害・過負荷 5xx InternalServerError 待って再試行
認証 401 AuthenticationError × APIキーと環境変数を確認
権限・地域 403 PermissionDeniedError × プロジェクトと権限を確認
見つからない 404 NotFoundError × モデル名・IDを確認
入力が不正 400 BadRequestError × リクエストの中身を直す

この表は最初の切り分け用です。429の行が2つあることからも分かるとおり、ステータスだけでは決まりませんexc.code まで見てください。

この記事で扱っていないこと

 ストリーミング

ストリーミングは、回答を少しずつ受け取りながら表示する方式です。すでに一部を受け取った後でエラーになることがあります。

このとき最初から自動で再実行すると、すでに画面へ出した文章と、やり直した後の文章が二重に出る可能性があります。公式ガイドでも、ストリームが始まって出力を処理した後は、リクエスト全体を安易に再実行しないよう注意されています。

この記事はストリーミングを使わない呼び出しを前提にしています。

 冪等性とジョブの再実行

冪等(べきとう)とは、同じ操作を2回やっても結果が1回分と同じになることです。文章を生成するだけなら問題になりませんが、生成した結果をデータベースへ書き込む処理まで含めて再試行すると、同じレポートが2件保存されることがあります。

再試行の対象はAPI呼び出しだけに限る、というのが基本です。保存まで含めた再実行は、ジョブ管理の話になります。

用途で変わる:対話型と夜間バッチ

「何回再試行すればよいか」に正解はありません。API側の都合ではなく、業務として何秒まで待てるかから決めます。

 画面で人が待っている場合

店舗スタッフが画面で質問しているなら、待たせられるのはせいぜい30秒です。

client = OpenAI(timeout=15.0)

呼ぶ(質問, 制限秒=30.0)
  → 失敗したら「一時的に混み合っています。少し待ってお試しください」

粘るより、早めに諦めて、もう一度押せることを伝えるほうが親切です。

 100店舗の夜間バッチ

人がその場で待っていないので、長めに粘れます。ただし1件の失敗で全体を止めないことのほうが重要です。

結果 どうするか
成功した店舗 そのまま保存
一時エラーで終わった店舗 一覧へ記録し、後でまとめて再処理
401・400などの店舗 設定の問題。再処理せず通知
429の残高・上限 バッチ全体を止める。他店舗も必ず失敗するため

最後の行が、この記事の内容がそのまま効くところです。残高が尽きたと分かった時点で、残り99店舗を試す意味はありません。 exc.code を見ていれば、1件目で判断できます。

1件失敗しただけで100店舗すべてを最初からやり直す必要はありません。

まとめ

APIエラーは、まず「待てば直る可能性があるか」で分けます。

そして、再試行そのものはすでにSDKが書いています

覚えておくこと
SDKがやること 接続エラー・408・409・429・5xx を既定で2回再試行。Retry-After にも従う
自分で書くこと 待っても直らない429(4種類)で、待たずにやめる
上限の決め方 回数ではなく時間(デッドライン)。回数だとSDKの分と掛け算になる
確かめ方 偽のクライアントで擬似的にエラーを起こして流す
ログ request ID・試行回数・error code。APIキーは絶対に残さない

下書きの段階でよくあるのが、SDKの自動再試行を止めて、自分で指数バックオフを書き直すことです。その時点で Retry-After への対応が消えます。 書き直すなら、SDKが何をしていたかを知ったうえで、同じことを実装してください。

Screenshot

【月1 特定テーマ講座(10月)】
Python で学ぶ 明日からできる「欠損値処理」超入門

【開催日時】 全2回(土)2026/10/17,10/31(13:30〜18:00)
【受講形式】 当日Zoom( or 復習用に後日動画視聴)
【参加費用】 2万2千円(税込み)/人