LLM API の 429(自分の上限超過)と 529(提供側の混雑)は、公式 SDK が初めから自動で送り直してくれます。最初にやることはリトライ処理を書くことではなく、SDK の max_retries と timeout を自分の用途に合わせて設定することです。

この記事では、Anthropic と OpenAI の公式 Python SDK を手元の模擬サーバーにつなぎ、429・529・タイムアウトのときに SDK が何回送り直し、何秒待つかを数えました。その結果をもとに、ステータス別のエラー一覧表、最悪の待ち時間の計算、retry-after を優先する待ち方、並列実行とストリーミングの扱いを、呼ぶ側の視点でまとめます。

確認環境(確認日 2026-10-06):Ubuntu 24.04、Python 3.11.15、anthropic 1.11.0、openai 3.24.0(どちらも公式 Python SDK)。実際の API は呼ばず、この記事に全文を載せた模擬サーバー(Python 標準ライブラリ製)に SDK をつないで実行しました。エラーの種類と意味は、Anthropic と OpenAI の公式ドキュメントを今回開いて確認した範囲です。

この記事の結論

  • 公式 SDK は初めから 2 回まで自動で送り直す。 対象は接続エラー・408・409・429・500 以上。まず max_retries と timeout を設定する。
  • 自前のリトライを外側に重ねない。 重ねると回数が掛け算になる(3 回 × 3 回で 9 回送ることを確認)。自前で待ち方を決めたいときは max_retries=0 にしてから書く。
  • 最悪の待ち時間は timeout ×(max_retries + 1)に、送り直す前の待ち時間を足した値。 初期値のままだと 10 分 × 3 回で 30 分を超える。
  • 429 は自分の使い方を見直す合図、529 は提供側が混んでいる合図。 429 は retry-after に従い、529 は間隔を空けて送り直す。
  • retry-after の無い 429 は、待っても通らないことがある。 利用額の上限や残高切れの可能性があるので、メッセージを読んで設定を見直す。

ステータス別エラー一覧の早見表

Anthropic の API が返すエラーです(公式ドキュメント「Errors」に記載のもの)。

ステータス エラーの種類 意味 送り直してよいか 対処
400 invalid_request_error リクエストの形式や内容に問題がある。自分で設定した利用額の上限に達したときも 400 になる 同じ内容では通らない メッセージを読んでリクエストを直す
401 authentication_error API キーに問題がある(形式違い・取り消し・期限切れなど) 同じ内容では通らない キーを確認して設定し直す
402 billing_error 請求や支払い情報に問題がある 同じ内容では通らない コンソールで支払い情報を確認する
403 permission_error そのキーには対象を使う権限が無い 同じ内容では通らない 組織とワークスペースの設定を確認する
404 not_found_error 対象が見つからない 同じ内容では通らない パスと ID を確認する
409 conflict_error 対象の現在の状態とぶつかっている 状態を解消してから 競合を解消して送り直す
413 request_too_large リクエストが大きすぎる(Messages API は 32 MB まで) 同じ内容では通らない 入力を小さくする・分ける
429 rate_limit_error 時間あたりの上限に達した。または利用額の上限に達した 時間あたりの上限なら送り直せる retry-after の秒数だけ待つ。retry-after が無ければ利用額を確認する
500 api_error 提供側の内部で想定外のエラーが起きた 送り直せる 指数バックオフで送り直す。続くときはリクエスト ID を添えて問い合わせる
504 timeout_error 処理中に時間切れになった 送り直せる 長い処理はストリーミングやバッチに切り替える
529 overloaded_error API が一時的に混み合っている 送り直せる 間隔を空けて送り直す

OpenAI の API が返すエラーです(公式ドキュメント「Error codes」に記載のもの)。

ステータス 名前 意味 送り直してよいか 対処
400 Invalid service_tier argument 指定したサービス階層がそのプロジェクトで使えない 同じ内容では通らない 使える階層を指定するか、プロジェクトの設定を変える
401 Invalid Authentication / Incorrect API key provided 認証情報や API キーが正しくない 同じ内容では通らない キーと組織を確認する。必要なら新しいキーを作る
401 Organization membership required アカウントが組織に属していない 同じ内容では通らない 組織に招待してもらう
401 IP not authorized 送信元の IP アドレスが許可リストに無い 同じ内容では通らない 許可された IP から送るか、許可リストを更新する
403 Unsupported region 対応していない国や地域からのアクセス 同じ内容では通らない 対応地域の一覧を確認する
429 Rate limit reached 短い時間に多くのリクエストやトークンを送った 送り直せる 送る速さを抑え、Retry-After に従い、指数バックオフを使う
429 Slow down 送る量を急に増やした 送り直せる 速さを下げ、少しずつ増やす。Retry-After に従う
429 Credit balance exhausted 前払いクレジットの残りが無い 待っても通らない クレジットを追加する
429 Organization spend limit / Project spend limit 組織またはプロジェクトの利用額の上限に達した 待っても通らない 上限を見直す
500 Server error 提供側のサーバーで問題が起きた 送り直せる 少し待って送り直す。状況ページを確認する
503 Model overloaded 指定したモデルが一時的に混み合っている 送り直せる Retry-After に従って送り直す

529 は Anthropic の API が使うステータスで、OpenAI では同じ「提供側の混雑」が 503 として返ります。

Python SDK が投げる例外との対応です。自動リトライの対象は両社の SDK ドキュメントの記載で、「1 回で返る」と書いた行と 429・529・時間切れの行は今回の実行でも確かめました。

ステータス 例外クラス(両 SDK 共通の名前) SDK の自動リトライ
400 BadRequestError しない(1 回で返る)
401 AuthenticationError しない(1 回で返る)
403 PermissionDeniedError しない
404 NotFoundError しない
409 ConflictError する
413 anthropic は RequestTooLargeError、openai は APIStatusError しない(1 回で返る)
422 UnprocessableEntityError しない
429 RateLimitError する
500 以上 InternalServerError する
529 anthropic は OverloadedError、openai は InternalServerError する
応答なし(時間切れ) APITimeoutError する
応答なし(接続できない) APIConnectionError する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「LLM APIの429・529エラー対処|リトライ実装とエラー一覧」

まず設定する2つ:max_retries と timeout

両社の SDK は同じ作りで、初期値も同じです。

設定 初期値 意味
max_retries 2 自動で送り直す回数。最初の 1 回と合わせて最大 3 回送る。0 で自動リトライを止める
timeout 10 分(接続は 5 秒) 応答を待つ時間。秒数(小数)か、接続・読み取りなどを分けた Timeout オブジェクトで渡す
待ち方 0.5 秒から倍々、最大 8 秒 送り直す前の待ち時間。retry-after があればその秒数を優先する

クライアント全体に設定する書き方と、1 回の呼び出しだけ変える書き方があります。掲載のコードは模擬サーバーにつなぐため api_key="dummy" と base_url を指定しています。実際の API を呼ぶときは、この 2 つの指定を外し、公式ドキュメントの手順で API キーを設定してください。

# settings.py  まず設定する 2 つ: max_retries と timeout
import anthropic, httpx2, openai

BASE = "http://127.0.0.1:10320/rate2"          # 模擬サーバー(最初の 2 回は 429 を返す)

# クライアント全体の設定
a_client = anthropic.Anthropic(
    api_key="dummy", base_url=BASE,
    max_retries=3,                              # 初期値は 2。0 で自動リトライを止める
    timeout=httpx2.Timeout(60.0, connect=5.0),  # 初期値は 10 分。接続だけ 5 秒に短く
)
o_client = openai.OpenAI(api_key="dummy", base_url=f"{BASE}/v1", max_retries=3, timeout=60.0)

# 1 回の呼び出しだけ変える(with_options)
message = a_client.with_options(max_retries=5, timeout=20.0).messages.create(
    model="mock", max_tokens=16, messages=[{"role": "user", "content": "hi"}])
print("anthropic:", message.content[0].text, "/ request id:", message._request_id)

# 応答ヘッダーを見たいときは with_raw_response
raw = o_client.chat.completions.with_raw_response.create(
    model="mock", messages=[{"role": "user", "content": "hi"}])
print("openai:", raw.parse().choices[0].message.content, "/ request-id ヘッダー:", raw.headers.get("request-id"))

実行結果:

anthropic: OK / request id: req_mock
openai: OK / request-id ヘッダー: req_mock

設定の目安は次のとおりです(数値は例です)。

使い方 max_retries timeout 理由
画面の前で人が待つ 1〜2 20〜60 秒 長く待たせるより、早めに知らせてやり直してもらう
夜間のまとめ処理 4〜6 2〜10 分 急がないので、混雑が引くまで粘る
自前で待ち方を決める 0 用途に合わせる リトライを 1 か所にまとめる(後述)
長い出力を受け取る 2 ストリーミングにする 無通信の時間が長いと、途中の通信経路で切れることがある

Anthropic の SDK は、ストリーミングを使わないリクエストが 10 分ほどを超えそうだと見積もると、送る前に ValueError を出します(SDK ドキュメント「Long requests」)。max_tokens を大きくするときはストリーミングに切り替えます。

SDKが何回送り直すかを数える

模擬サーバー(全文は記事の後半)に、SDK から同じリクエストを送り、サーバーが受け取った回数・間隔・最後に返った例外を記録しました。

# probe.py  公式 SDK が何回送り直し、どれだけ待つかを模擬サーバーで数える
import json, time, urllib.request
import anthropic, openai

BASE = "http://127.0.0.1:10320"

def call_anthropic(scenario, **options):
    client = anthropic.Anthropic(api_key="dummy", base_url=f"{BASE}/{scenario}", **options)
    return client.messages.create(model="mock", max_tokens=16,
                                  messages=[{"role": "user", "content": "hi"}])

def call_openai(scenario, **options):
    client = openai.OpenAI(api_key="dummy", base_url=f"{BASE}/{scenario}/v1", **options)
    return client.chat.completions.create(model="mock",
                                          messages=[{"role": "user", "content": "hi"}])

def measure(label, call, scenario, **options):
    urllib.request.urlopen(f"{BASE}/_reset").read()
    start = time.monotonic()
    try:
        call(scenario, **options)
        result = "成功"
    except Exception as e:                      # 例外の型を見るための確認用
        result = type(e).__name__
    elapsed = time.monotonic() - start
    stats = json.load(urllib.request.urlopen(f"{BASE}/_stats"))
    times = [row[0] for row in stats["log"]]
    gaps = [round(b - a, 1) for a, b in zip(times, times[1:])]
    print(f"{label:<30} 受信 {len(times)} 回  {elapsed:4.1f} 秒  間隔 {gaps}  → {result}")

print("anthropic", anthropic.__version__, "/ openai", openai.__version__)
for name, call in (("anthropic", call_anthropic), ("openai", call_openai)):
    print(f"--- {name}")
    measure("429 が 2 回(retry-after: 2)", call, "rate2")
    measure("429 が続く(retry-after 無し)", call, "cap429")
    measure("529 が続く", call, "over529")
    measure("529 が続く max_retries=4", call, "over529", max_retries=4)
    measure("529 が続く max_retries=0", call, "over529", max_retries=0)
    measure("400", call, "bad400")
    measure("401", call, "auth401")
    measure("413", call, "big413")
    measure("5 秒かかる応答 timeout=1", call, "slow", timeout=1.0)
    measure("同上 max_retries=0", call, "slow", timeout=1.0, max_retries=0)

実行結果:

anthropic 1.11.0 / openai 3.24.0
--- anthropic
429 が 2 回(retry-after: 2)      受信 3 回   4.1 秒  間隔 [2.0, 2.0]  → 成功
429 が続く(retry-after 無し)        受信 3 回   1.5 秒  間隔 [0.5, 1.0]  → RateLimitError
529 が続く                        受信 3 回   1.4 秒  間隔 [0.4, 1.0]  → OverloadedError
529 が続く max_retries=4          受信 5 回   6.7 秒  間隔 [0.5, 0.9, 1.8, 3.5]  → OverloadedError
529 が続く max_retries=0          受信 1 回   0.0 秒  間隔 []  → OverloadedError
400                            受信 1 回   0.0 秒  間隔 []  → BadRequestError
401                            受信 1 回   0.0 秒  間隔 []  → AuthenticationError
413                            受信 1 回   0.0 秒  間隔 []  → RequestTooLargeError
5 秒かかる応答 timeout=1             受信 3 回   4.3 秒  間隔 [1.4, 1.9]  → APITimeoutError
同上 max_retries=0               受信 1 回   1.0 秒  間隔 []  → APITimeoutError
--- openai
429 が 2 回(retry-after: 2)      受信 3 回   4.1 秒  間隔 [2.0, 2.0]  → 成功
429 が続く(retry-after 無し)        受信 3 回   1.4 秒  間隔 [0.4, 0.9]  → RateLimitError
529 が続く                        受信 3 回   1.4 秒  間隔 [0.4, 1.0]  → InternalServerError
529 が続く max_retries=4          受信 5 回   6.8 秒  間隔 [0.4, 0.8, 1.7, 3.9]  → InternalServerError
529 が続く max_retries=0          受信 1 回   0.0 秒  間隔 []  → InternalServerError
400                            受信 1 回   0.0 秒  間隔 []  → BadRequestError
401                            受信 1 回   0.0 秒  間隔 []  → AuthenticationError
413                            受信 1 回   0.0 秒  間隔 []  → APIStatusError
5 秒かかる応答 timeout=1             受信 3 回   4.3 秒  間隔 [1.5, 1.8]  → APITimeoutError
同上 max_retries=0               受信 1 回   1.0 秒  間隔 []  → APITimeoutError

読み取れることは 4 つです。

  1. retry-after があると、SDK はその秒数どおりに待つ。 「retry-after: 2」を 2 回返すと、間隔はちょうど 2.0 秒ずつになり、3 回目で成功しました。呼ぶ側のコードは何も書いていません。
  2. retry-after が無いと、0.5 秒・1 秒・2 秒・4 秒と倍々で待つ。 実際の間隔は少し短め(0.4、0.9、1.8、3.5 秒)で、これは SDK が待ち時間を最大 25% 短くするばらつき(ジッター)を入れているためです。
  3. 400・401・413 は 1 回で返る。 直さないと通らないエラーなので、SDK は送り直しません。
  4. タイムアウトも送り直しの対象。 timeout=1 のとき、1 秒で打ち切る → 待つ → 送り直す、を 3 回行い、合計 4.3 秒かかりました。

最悪の待ち時間を計算する

呼び出しが戻ってくるまでの最悪の時間は、次の式で見積もれます。

最悪の待ち時間 = timeout ×(max_retries + 1)+ 送り直す前の待ち時間の合計

送り直す前の待ち時間は、retry-after が無い場合で最大 0.5 秒、1 秒、2 秒、4 秒、8 秒、以降は 8 秒ずつです。

timeout max_retries timeout ×(回数) 待ち時間の合計(最大) 最悪の待ち時間
600 秒(初期値) 2(初期値) 600 × 3 = 1,800 秒 0.5 + 1 = 1.5 秒 1,801.5 秒(約 30 分)
30 秒 2 30 × 3 = 90 秒 1.5 秒 91.5 秒
20 秒 5 20 × 6 = 120 秒 0.5 + 1 + 2 + 4 + 8 = 15.5 秒 135.5 秒
60 秒 0 60 × 1 = 60 秒 0 秒 60 秒

先ほどの実行結果でも、timeout=1・max_retries=2 のとき 1 × 3 = 3 秒に待ち時間(0.4 秒と 0.9 秒)が加わって 4.3 秒でした。初期値のまま画面から呼ぶと、利用者が最大で約 30 分待つ計算になります。 人が待つ処理では、timeout を先に短くします。

timeout は全体の締め切りではない

timeout に秒数を 1 つ渡した場合、それは「次のデータが届くまで待つ時間」として働きます。3 秒かけて少しずつ返す応答を timeout=1.0 で受けたところ、時間切れにならず最後まで受け取れました。

# drip.py  timeout は「全体の締め切り」ではなく、通信の区切りごとの待ち時間
import time
import anthropic

client = anthropic.Anthropic(api_key="dummy", base_url="http://127.0.0.1:10320/drip",
                             timeout=1.0, max_retries=0)
start = time.monotonic()
message = client.messages.create(model="mock", max_tokens=16,
                                 messages=[{"role": "user", "content": "hi"}])
print(f"timeout=1.0 でも成功: {message.content[0].text} / {time.monotonic() - start:.1f} 秒")

実行結果:

timeout=1.0 でも成功: OK / 2.4 秒

「必ず 30 秒以内に戻す」のような全体の締め切りが必要なときは、SDK の timeout とは別に、呼ぶ側で開始時刻からの経過時間を見て打ち切ります(後述のコードの deadline がその例です)。

自前のリトライを重ねるとどうなるか

SDK の自動リトライを知らずに、外側に for 文のリトライを書くと、回数が掛け算になります。

# stacked.py  SDK の自動リトライの外側に、自前のリトライを重ねるとどうなるか
import json, time, urllib.request
import anthropic

BASE = "http://127.0.0.1:10320"
client = anthropic.Anthropic(api_key="dummy", base_url=f"{BASE}/over529")  # max_retries は初期値の 2

start = time.monotonic()
for attempt in range(3):                      # 自前で 3 回まで試す
    try:
        client.messages.create(model="mock", max_tokens=16,
                               messages=[{"role": "user", "content": "hi"}])
        break
    except anthropic.APIStatusError as e:
        print(f"自前の {attempt + 1} 回目: {type(e).__name__}")
        time.sleep(1)

stats = json.load(urllib.request.urlopen(f"{BASE}/_stats"))
print("サーバーが受け取った回数:", stats["count"]["over529"])
print(f"かかった時間: {time.monotonic() - start:.1f} 秒")

実行結果:

自前の 1 回目: OverloadedError
自前の 2 回目: OverloadedError
自前の 3 回目: OverloadedError
サーバーが受け取った回数: 9
かかった時間: 7.0 秒

自前では 3 回のつもりでも、サーバーには 3 × 3 = 9 回届いています。混雑しているときに送る数が 3 倍になり、OpenAI のドキュメントが述べるとおり、通らなかったリクエストも分あたりの上限に数えられるため、回復がかえって遅くなります。

リトライは 1 か所にまとめます。 選び方は次のとおりです。

条件 選ぶもの
特別な要件が無い SDK の自動リトライに任せ、max_retries と timeout だけ設定する
全体の締め切りや、待ち方の記録が必要 max_retries=0 にして、自前のリトライを 1 つ書く
ライブラリやフレームワーク越しに SDK を使っている そのライブラリ側のリトライ設定を確認し、どちらか片方だけ有効にする

429と529の違い、上限超過と残高切れの見分け方

観点 429(時間あたりの上限) 429(利用額の上限・残高切れ) 529 / 503(提供側の混雑)
原因 自分の組織が、分あたりのリクエスト数やトークン数の上限を超えた 月の利用額の上限に達した、前払いクレジットが無い 全利用者のアクセスが集中している
待てば通るか 通る 通らない(上限の見直しや入金が必要) 通る
retry-after 付く(Anthropic は待つ秒数を返す) Anthropic では付かない OpenAI の 503 は Retry-After に従うと案内されている
呼ぶ側でできること 送る速さと同時実行数を下げる 設定画面で上限と残高を確認する 間隔を空けて送り直す。急ぎでなければ時間をずらす

Anthropic のドキュメントによると、時間あたりの上限は 3 種類(分あたりのリクエスト数、入力トークン数、出力トークン数)で、容量は一定間隔で一気に戻るのではなく連続的に回復します。利用額の上限に達した 429 は、エラーの種類は同じ rate_limit_error のまま、retry-after ヘッダーが付かず、利用が再開されるまで通りません。OpenAI では、同じ 429 が「Rate limit reached」「Credit balance exhausted」「Organization spend limit」のように別の名前で返り、メッセージで区別できます。

そのため、429 を受け取ったら retry-after の有無とメッセージを見るのが確実です。

# classify.py  同じ 429 でも「待てば通る」か「待っても通らない」かを見分ける
import anthropic

def explain_429(error):
    retry_after = error.response.headers.get("retry-after")
    if retry_after:
        return f"時間あたりの上限。{retry_after} 秒待ってから送り直す"
    message = error.body["error"]["message"]
    return f"retry-after が無い。利用額の上限の可能性があるのでメッセージを読む: {message}"

for scenario in ("rate429", "cap429"):
    client = anthropic.Anthropic(api_key="dummy", max_retries=0,
                                 base_url=f"http://127.0.0.1:10320/{scenario}")
    try:
        client.messages.create(model="mock", max_tokens=16,
                               messages=[{"role": "user", "content": "hi"}])
    except anthropic.RateLimitError as e:
        print(e.status_code, e.body["error"]["type"], "/", explain_429(e))

実行結果:

429 rate_limit_error / 時間あたりの上限。1 秒待ってから送り直す
429 rate_limit_error / retry-after が無い。利用額の上限の可能性があるのでメッセージを読む: You have reached your API usage limits

なお、利用が急に増えたときにも 429 が返ることがあります(Anthropic のドキュメントでは acceleration limits、OpenAI では Slow down)。どちらも、送る量を少しずつ増やすことが対処として挙げられています。

残りの枠をヘッダーで見る

上限に近づいているかどうかは、応答ヘッダーで分かります。with_raw_response を使うと、SDK からヘッダーを読めます。

提供元 ヘッダー 内容
Anthropic retry-after 送り直すまでに待つ秒数
Anthropic anthropic-ratelimit-requests-remaining 上限までに送れる残りのリクエスト数
Anthropic anthropic-ratelimit-input-tokens-remaining 残りの入力トークン数(千単位に丸めた値)
Anthropic anthropic-ratelimit-output-tokens-remaining 残りの出力トークン数(千単位に丸めた値)
Anthropic anthropic-ratelimit-requests-reset リクエスト数の枠が完全に回復する時刻(RFC 3339 形式)
OpenAI Retry-After 送り直すまでに最低限待つ秒数
OpenAI x-ratelimit-remaining-requests 残りのリクエスト数
OpenAI x-ratelimit-remaining-tokens 残りのトークン数
OpenAI x-ratelimit-reset-requests リクエスト数の枠が戻るまでの時間

retry-afterを優先してジッター付きで待つコード

全体の締め切りを持たせたい、待った時間を記録したい、という場合は自前で書きます。ポイントは 4 つです。

  • SDK 側は max_retries=0 にする(二重にしない)。
  • retry-after があれば、その秒数を優先する。 複数の処理が同じ瞬間に再開しないよう、少しだけランダムに上乗せする。
  • retry-after が無ければ、ジッター付きの指数バックオフで待つ。
  • 送り直すステータスを絞り、回数と全体の締め切りの両方で打ち切る。
# retry_after.py  retry-after を優先し、無いときはジッター付きの指数バックオフで待つ
import random, time
import anthropic

RETRY_STATUS = {408, 409, 429, 500, 502, 503, 504, 529}

def wait_seconds(error, attempt, base=0.5, cap=30.0):
    """次に待つ秒数を決める。attempt は 0 始まり。"""
    header = None
    if isinstance(error, anthropic.APIStatusError):
        header = error.response.headers.get("retry-after")
    if header:
        try:
            # サーバーの指示どおり待ち、同時に再開しないよう少しだけ上乗せする
            return float(header) + random.uniform(0, 0.5)
        except ValueError:
            pass
    # 指示が無いとき: 0〜(base×2^attempt) の間でランダムに待つ(フルジッター)
    return random.uniform(0, min(cap, base * 2 ** attempt))

def call_with_retry(client, max_attempts=4, deadline=60.0, **params):
    start = time.monotonic()
    for attempt in range(max_attempts):
        try:
            return client.messages.create(**params)
        except anthropic.APIStatusError as e:
            if e.status_code not in RETRY_STATUS:
                raise                                   # 400・401 などは直してから送り直す
            error = e
        except anthropic.APIConnectionError as e:       # タイムアウトもここに含まれる
            error = e
        wait = wait_seconds(error, attempt)
        last = attempt == max_attempts - 1
        over = time.monotonic() - start + wait > deadline
        print(f"{attempt + 1} 回目: {type(error).__name__} "
              + ("→ 打ち切り" if last or over else f"→ {wait:.2f} 秒待つ"))
        if last or over:
            raise error
        time.sleep(wait)

if __name__ == "__main__":
    params = dict(model="mock", max_tokens=16, messages=[{"role": "user", "content": "hi"}])
    for scenario in ("rate2", "over2", "bad400"):
        # 自前で待つので SDK 側の自動リトライは 0 にする(二重にしない)
        client = anthropic.Anthropic(api_key="dummy", max_retries=0, timeout=10.0,
                                     base_url=f"http://127.0.0.1:10320/{scenario}")
        print(f"--- {scenario}")
        try:
            message = call_with_retry(client, **params)
            print("成功:", message.content[0].text)
        except anthropic.APIStatusError as e:
            print("そのまま返す:", e.status_code, type(e).__name__)

実行結果(待つ秒数は乱数を含むため、実行のたびに変わります):

--- rate2
1 回目: RateLimitError → 2.23 秒待つ
2 回目: RateLimitError → 2.28 秒待つ
成功: OK
--- over2
1 回目: OverloadedError → 0.49 秒待つ
2 回目: OverloadedError → 0.69 秒待つ
成功: OK
--- bad400
そのまま返す: 400 BadRequestError

429 のときは retry-after の 2 秒に上乗せして待ち、529 のときは短い待ち時間から始め、400 は送り直さずにそのまま返しています。OpenAI の SDK でも例外クラスの名前とヘッダーの読み方は同じなので、anthropic を openai に、messages.create を chat.completions.create に置き換えれば同じ形で書けます。

並列実行:セマフォで同時実行数を抑える

たくさんのリクエストを非同期で一度に送ると、時間あたりの上限にすぐ届きます。セマフォ(同時に通れる数を決める仕組み)で、送る数を先に絞ります。 模擬サーバーは「同時に 4 件以上届いたら 429 を返す」設定です。

# semaphore.py  同時に送る数をセマフォで抑える(12 件を送る。サーバーは同時 4 件以上で 429 を返す)
import asyncio, json, time, urllib.request
import anthropic

BASE = "http://127.0.0.1:10320"

async def run(limit):
    urllib.request.urlopen(f"{BASE}/_reset").read()
    client = anthropic.AsyncAnthropic(api_key="dummy", base_url=f"{BASE}/limit3", max_retries=2)
    sem = asyncio.Semaphore(limit)

    async def one(i):
        async with sem:                       # ここで同時実行数が limit までに絞られる
            await client.messages.create(model="mock", max_tokens=16,
                                         messages=[{"role": "user", "content": f"質問 {i}"}])

    start = time.monotonic()
    results = await asyncio.gather(*(one(i) for i in range(12)), return_exceptions=True)
    await client.close()
    stats = json.load(urllib.request.urlopen(f"{BASE}/_stats"))
    errors = sum(isinstance(r, Exception) for r in results)
    print(f"同時 {limit:2} 件まで: 成功 {12 - errors:2} 件 / エラー {errors} 件 / "
          f"サーバーの受信 {stats['count']['limit3']:2} 回 / 同時処理の最大 {stats['peak']:2} / "
          f"{time.monotonic() - start:.1f} 秒")

asyncio.run(run(12))   # 絞らない(12 件を一度に送る)
asyncio.run(run(3))    # 3 件までに絞る

実行結果(絞らない場合の件数は、実行のたびに変わります):

同時 12 件まで: 成功 11 件 / エラー 1 件 / サーバーの受信 27 回 / 同時処理の最大  6 / 3.5 秒
同時  3 件まで: 成功 12 件 / エラー 0 件 / サーバーの受信 12 回 / 同時処理の最大  3 / 2.2 秒

12 件を一度に送ると、429 と自動リトライが繰り返されてサーバーには 27 回届き、それでも 1 件はエラーで終わりました。3 件までに絞ると、12 回ちょうどで全件が成功し、かかった時間も短くなっています。リトライで取り戻すより、先に絞るほうが速くて確実です。

同時実行数の決め方の目安です。

状況 決め方
上限が分かっている 「分あたりのリクエスト数の上限 × 1 件の平均所要秒数 ÷ 60」より小さくする(例:上限が毎分 60 件、1 件 10 秒なら 10 未満)
上限が分からない 小さい数(2〜4)から始め、429 が出ないことを見ながら増やす
トークン数の上限に先に届く 同時実行数だけでなく、1 件あたりの入力の大きさも抑える
急がない大量の処理 まとめて投入して結果をあとで受け取る方式を検討する(Anthropic では Message Batches API が案内されている)

ストリーミング途中で切れたときの扱い

ストリーミングでは、ステータス 200 を返したあとでエラーになることがあります。Anthropic のドキュメントには、混雑時にイベントの流れの中で次のようなエラーイベントが届くと書かれています。

event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}

このとき SDK の自動リトライは働きません。模擬サーバーで 3 通りの止まり方を再現したところ、次の結果でした。

止まり方 SDK の挙動 サーバーの受信回数
途中でエラーイベントが届く anthropic.APIStatusError が出る。status_code は 200 のまま(OverloadedError にはならない) 1 回(送り直さない)
終わりの印が無いまま接続が切れる httpx2.RemoteProtocolError が出る(SDK の例外クラスではない) 1 回(送り直さない)
終了イベント(message_stop)が無いまま本文だけ終わる 例外は出ない。stop_reason が None のまま 1 回

つまり、ストリーミングでは 「例外が出なかった」だけで完了と判断せず、stop_reason が入っているかを確かめる必要があります。

Anthropic のドキュメントは、途中まで届いた内容を保存し、続きを頼むリクエストを送る方法を挙げています。Claude 4.6 以降のモデルでは、届いた内容と「続きを書いてほしい」という指示を user メッセージに入れます(Claude 4.5 以前では、届いた内容を assistant メッセージの書き出しとして渡します)。また、ツール呼び出しと拡張思考のブロックは途中から再開できず、再開できるのは直近のテキストブロックからです。

# stream_resume.py  ストリーミングの途中で切れたとき、届いた分を残して続きを頼む
import anthropic, httpx2

def stream_once(client, messages):
    """届いた文字を貯めながら読む。戻り値は (本文, 完了したか, 止まった理由)。"""
    text = ""
    try:
        with client.messages.stream(model="mock", max_tokens=256, messages=messages) as stream:
            for chunk in stream.text_stream:
                text += chunk
            stop_reason = stream.current_message_snapshot.stop_reason
        # 例外が出なくても、stop_reason が入っていなければ最後まで届いていない
        return text, stop_reason is not None, f"stop_reason={stop_reason}"
    except (anthropic.APIError, httpx2.HTTPError) as e:
        return text, False, type(e).__name__

def ask(make_client, question):
    messages = [{"role": "user", "content": question}]
    text, done, error = stream_once(make_client(0), messages)
    tries = 1
    while not done and tries < 3:
        print(f"途中で止まった: {error} / 届いた分: {text!r}")
        # 届いた分を渡して、続きだけを書いてもらう
        follow = messages + [{"role": "user", "content":
            f"Your previous response was interrupted and ended with {text}. "
            "Continue from where you left off."}]
        more, done, error = stream_once(make_client(tries), follow)
        text += more
        tries += 1
    return text, done

if __name__ == "__main__":
    for first in ("sse_error", "sse_cut", "sse_eof"):
        # 1 回目は途中で止まる応答、2 回目からは最後まで届く応答を返す模擬サーバーにつなぐ
        def make_client(n, first=first):
            scenario = first if n == 0 else "sse_ok"
            return anthropic.Anthropic(api_key="dummy", base_url=f"http://127.0.0.1:10320/{scenario}")
        print(f"--- {first}")
        text, done = ask(make_client, "設定の手順を教えて")
        print("完了:", done, "/ 送った回数:", 2 if done else 3)

実行結果:

--- sse_error
途中で止まった: APIStatusError / 届いた分: '手順は3つです。1. 設定を開く。'
完了: True / 送った回数: 2
--- sse_cut
途中で止まった: RemoteProtocolError / 届いた分: '手順は3つです。1. 設定を開く。'
完了: True / 送った回数: 2
--- sse_eof
途中で止まった: stop_reason=None / 届いた分: '手順は3つです。1. 設定を開く。'
完了: True / 送った回数: 2

この実行は、止まったことを検知して 2 回目を送る流れの確認です。模擬サーバーは決まった文を返すだけなので、続きの文章の出来ばえは実際のモデルで確かめてください。

扱い方の選び方です。

条件 選ぶもの
出力が短い・すぐやり直せる 届いた分を捨てて、最初から送り直す
出力が長い・テキストだけ 届いた分を残して、続きを頼む
ツール呼び出しの途中で止まった そのブロックは捨てて、最初から送り直す
画面に途中まで表示している 表示を残したまま「続きを取得中」と知らせ、続きをつなぐ

模擬サーバーの全文

この記事の実行結果は、すべて次のサーバーに SDK をつないで得たものです。URL の先頭の文字列(rate2、over529 など)で返し方を切り替えます。自分のリトライ処理を試す台としても使えます。

# mock_server.py  LLM API の代わりに 429・529・遅延・途中で切れるストリームを返す模擬サーバー
# 使い方: python3 mock_server.py 10320
# 動作は URL の先頭で切り替える(例: http://127.0.0.1:10320/rate2/v1/messages)
import json, sys, threading, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

LOCK = threading.Lock()
COUNT = {}          # シナリオごとの受信回数
ACTIVE = 0          # いま処理中のリクエスト数
PEAK = 0            # 同時処理数の最大
LOG = []            # (経過秒, シナリオ, 何回目, x-stainless-retry-count)
T0 = time.monotonic()

def ok_body(path):
    if path.endswith("/messages"):
        return {"id": "msg_mock", "type": "message", "role": "assistant", "model": "mock",
                "content": [{"type": "text", "text": "OK"}], "stop_reason": "end_turn",
                "stop_sequence": None, "usage": {"input_tokens": 5, "output_tokens": 1}}
    return {"id": "chatcmpl-mock", "object": "chat.completion", "created": 0, "model": "mock",
            "choices": [{"index": 0, "finish_reason": "stop",
                         "message": {"role": "assistant", "content": "OK"}}],
            "usage": {"prompt_tokens": 5, "completion_tokens": 1, "total_tokens": 6}}

def err(kind, message):
    return {"type": "error", "error": {"type": kind, "message": message}, "request_id": "req_mock"}

class H(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"
    def log_message(self, *a): pass

    def send_json(self, status, body, headers=None):
        data = json.dumps(body).encode()
        self.send_response(status)
        self.send_header("content-type", "application/json")
        self.send_header("content-length", str(len(data)))
        self.send_header("request-id", "req_mock")
        for k, v in (headers or {}).items():
            self.send_header(k, v)
        self.end_headers()
        self.wfile.write(data)

    def do_GET(self):
        global PEAK
        if self.path == "/_stats":
            with LOCK:
                self.send_json(200, {"count": COUNT, "peak": PEAK, "log": LOG})
        elif self.path == "/_reset":
            with LOCK:
                COUNT.clear(); LOG.clear(); PEAK = 0
            self.send_json(200, {"reset": True})

    def do_POST(self):
        global ACTIVE, PEAK
        n = int(self.headers.get("content-length", 0))
        req = json.loads(self.rfile.read(n) or b"{}")
        scenario = self.path.split("/")[1]
        with LOCK:
            COUNT[scenario] = COUNT.get(scenario, 0) + 1
            nth = COUNT[scenario]
            ACTIVE += 1
            PEAK = max(PEAK, ACTIVE)
            active_now = ACTIVE
            LOG.append((round(time.monotonic() - T0, 2), scenario, nth,
                        self.headers.get("x-stainless-retry-count")))
        try:
            self.route(scenario, nth, active_now, req)
        except (BrokenPipeError, ConnectionResetError):
            pass
        finally:
            with LOCK:
                ACTIVE -= 1

    def route(self, scenario, nth, active_now, req):
        if scenario == "rate2" and nth <= 2:        # 2回だけ 429(retry-after: 2)
            return self.send_json(429, err("rate_limit_error", "rate limited"), {"retry-after": "2"})
        if scenario == "rate429":                   # いつも 429(retry-after: 1)
            return self.send_json(429, err("rate_limit_error", "rate limited"), {"retry-after": "1"})
        if scenario == "cap429":                    # 上限額に達した形の 429(retry-after なし)
            return self.send_json(429, err("rate_limit_error", "You have reached your API usage limits"))
        if scenario == "over529":                   # いつも 529(retry-after なし)
            return self.send_json(529, err("overloaded_error", "Overloaded"))
        if scenario == "over2" and nth <= 2:        # 2回だけ 529
            return self.send_json(529, err("overloaded_error", "Overloaded"))
        if scenario == "bad400":
            return self.send_json(400, err("invalid_request_error", "max_tokens: Field required"))
        if scenario == "auth401":
            return self.send_json(401, err("authentication_error", "invalid x-api-key"))
        if scenario == "big413":
            return self.send_json(413, err("request_too_large", "Request exceeds the maximum size"))
        if scenario == "slow":                      # 5秒かかる応答
            time.sleep(5)
        if scenario == "drip":                      # 0.6秒おきに少しずつ、合計3秒かけて返す
            data = json.dumps(ok_body(self.path)).encode()
            self.send_response(200)
            self.send_header("content-type", "application/json")
            self.send_header("content-length", str(len(data)))
            self.end_headers()
            step = len(data) // 5 + 1
            for i in range(0, len(data), step):
                self.wfile.write(data[i:i + step]); self.wfile.flush()
                time.sleep(0.6)
            return
        if scenario == "limit3":                    # 同時に4件以上来たら 429
            if active_now > 3:
                return self.send_json(429, err("rate_limit_error", "too many concurrent"), {"retry-after": "1"})
            time.sleep(0.5)
        if scenario in ("sse_error", "sse_cut", "sse_eof") or (scenario == "sse_ok" and req.get("stream")):
            return self.stream(scenario)
        self.send_json(200, ok_body(self.path))

    def stream(self, scenario):
        self.send_response(200)
        self.send_header("content-type", "text/event-stream")
        self.send_header("request-id", "req_mock")
        self.send_header("transfer-encoding", "chunked")
        self.end_headers()
        def ev(name, data):
            body = f"event: {name}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n".encode()
            self.wfile.write(f"{len(body):x}\r\n".encode() + body + b"\r\n")
            self.wfile.flush()
        ev("message_start", {"type": "message_start", "message": {
            "id": "msg_mock", "type": "message", "role": "assistant", "model": "mock", "content": [],
            "stop_reason": None, "stop_sequence": None, "usage": {"input_tokens": 5, "output_tokens": 0}}})
        ev("content_block_start", {"type": "content_block_start", "index": 0,
                                   "content_block": {"type": "text", "text": ""}})
        parts = ["手順は3つです。", "1. 設定を開く。", "2. 値を入力する。", "3. 保存する。"]
        stop_at = len(parts) if scenario == "sse_ok" else 2
        for p in parts[:stop_at]:
            ev("content_block_delta", {"type": "content_block_delta", "index": 0,
                                       "delta": {"type": "text_delta", "text": p}})
            time.sleep(0.1)
        if scenario == "sse_error":                 # 200 を返したあとでエラーイベント
            ev("error", {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}})
        elif scenario == "sse_cut":                 # 終わりの印を送らずに接続が切れる
            self.close_connection = True
            return
        elif scenario == "sse_eof":                 # 終了イベントを送らずに本文だけ正常に閉じる
            pass
        else:
            ev("content_block_stop", {"type": "content_block_stop", "index": 0})
            ev("message_delta", {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": None},
                                 "usage": {"output_tokens": 20}})
            ev("message_stop", {"type": "message_stop"})
        self.wfile.write(b"0\r\n\r\n")
        self.close_connection = True

if __name__ == "__main__":
    port = int(sys.argv[1]) if len(sys.argv) > 1 else 10320
    ThreadingHTTPServer(("127.0.0.1", port), H).serve_forever()

起動と確認の手順:

python3 mock_server.py 10320 &
python3 probe.py

指数バックオフ・レート制限の実装との違い

似た話題と、この記事の範囲を整理します。

話題 立場 主な内容 この記事との関係
指数バックオフ どの API にも共通の考え方 待ち時間の式、ジッターの種類、冪等性 SDK が内部で行っている待ち方の元になる考え方
レート制限の実装 API を提供する側 上限を数えるアルゴリズム、429 の返し方 429 と retry-after を返す側の仕組み
LLM API のエラー対処(この記事) API を呼ぶ側 SDK の設定、ステータス別の対処、並列とストリーミング 上の 2 つを前提に、設定値と手順を決める

どこから手を付けるかの判断基準です。

いま起きていること 最初に見るもの
ときどき 429 や 529 が出るが、たいてい通る SDK の max_retries を 1〜2 増やす
並列で流すと 429 が続く セマフォで同時実行数を絞る
429 が出続けて、待っても通らない retry-after の有無とメッセージ。利用額の上限と残高
応答が返らず、長く固まる timeout と、最悪の待ち時間の計算
400 番台(429 以外)が出る リクエストの内容・キー・権限。送り直しでは直らない
長い出力の途中で切れる ストリーミングと stop_reason の確認

確かめて分かったこと

  • 両社の SDK は、初期値(2 回・10 分・0.5 秒から最大 8 秒)も待ち方も同じだった。 retry-after を返すと、どちらもその秒数どおり(2.0 秒)に待った。
  • 自前の 3 回リトライを重ねると、サーバーには 9 回届いた。 回数は足し算ではなく掛け算になる。
  • 529 を OpenAI の SDK に返すと InternalServerError になり、Anthropic の SDK では OverloadedError になった。 500 以上をまとめて捕まえるなら、どちらの SDK でも InternalServerError で受けられる。
  • timeout=1.0 でも、少しずつ届く応答は 2.4 秒かけて最後まで受け取れた。 timeout は全体の締め切りではない。
  • ストリーミング途中のエラーイベントは、status_code が 200 の APIStatusError として届き、自動では送り直されなかった。
  • 終了イベントが無いまま本文が終わる応答では、例外が出なかった。 完了の判定には stop_reason を使う。

よくある質問

Q. 429 エラーとは何ですか? A. 自分の組織が、時間あたりの上限か利用額の上限に達したことを示すエラーです。retry-after ヘッダーがあればその秒数だけ待てば通り、無い場合は利用額の上限や残高を確認します。

Q. 429 と 529 の違いは何ですか? A. 429 は自分の使い方が原因、529 は提供側の混雑が原因です。429 は送る速さと同時実行数を見直し、529 は間隔を空けて送り直します。OpenAI では混雑が 503 として返ります。

Q. リトライ処理は自分で書く必要がありますか? A. 多くの場合は不要です。公式 SDK が接続エラー・408・409・429・500 以上を初めから 2 回まで送り直すので、max_retries と timeout の設定で足ります。自前で書くときは max_retries=0 にして、リトライを 1 か所にまとめます。

Q. リトライしても 429 が直らないときはどうすればよいですか? A. retry-after ヘッダーが付いているかを確認します。付いていなければ、待っても通らない種類(利用額の上限、残高切れ)の可能性が高いので、エラーメッセージを読み、管理画面で上限と残高を見直します。付いている場合は、同時実行数を下げます。

Q. タイムアウトは何秒にすればよいですか? A. 「利用者が待てる時間 ÷(max_retries + 1)」より短くするのが目安です。たとえば 90 秒まで待てて max_retries が 2 なら、timeout は 30 秒弱にします。長い出力はストリーミングに切り替えます。

まとめ

  • 公式 SDK は初めから自動でリトライする。 まず max_retries と timeout を設定する。
  • 最悪の待ち時間は timeout ×(max_retries + 1)+ 待ち時間。 初期値のままだと約 30 分になる。
  • 自前のリトライは重ねない。 書くなら max_retries=0 にして 1 か所にまとめる。
  • 429 は retry-after に従う。 retry-after が無ければ、利用額の上限や残高を確認する。
  • 529(OpenAI では 503)は提供側の混雑。 間隔を空けて送り直す。
  • 並列実行はセマフォで先に絞る。 リトライで取り戻すより速く、確実に通る。
  • ストリーミングは 200 のあとでも止まる。 stop_reason で完了を確かめ、届いた分を残して続きを頼む。

参考・出典

  • Anthropic「Errors」 https://platform.claude.com/docs/en/api/errors (2026-10-06 確認)
  • Anthropic「Rate limits」 https://platform.claude.com/docs/en/api/rate-limits (2026-10-06 確認)
  • Anthropic「Streaming Messages」 https://platform.claude.com/docs/en/build-with-claude/streaming (2026-10-06 確認)
  • Anthropic「Python SDK」 https://platform.claude.com/docs/en/api/sdks/python (2026-10-06 確認)
  • OpenAI「Error codes」 https://developers.openai.com/api/docs/guides/error-codes (2026-10-06 確認)
  • OpenAI「Rate limits」 https://developers.openai.com/api/docs/guides/rate-limits (2026-10-06 確認)
  • OpenAI Python ライブラリ README https://github.com/openai/openai-python (2026-10-06 確認)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「LLM APIの429・529エラー対処|リトライ実装とエラー一覧」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?