個人開発のアプリに付けている小さなバックエンドのジョブで、エラー処理を書き足していた夜、私自身は「上限に達したら 429 が返る」と思い込んだまま、429 だけを見張る分岐を組んでいました。月の利用上限を自分で決めておけば、超えたときは 429 で気づけるはずだ、と考えていたのです。
公式の rate limits のページを読み直して、その前提が半分しか合っていないことに気づきました。自分で設定した上限に達したときは、429 ではなく 400 が返ります。しかも、同じ 429 でも「待てば通るもの」と「月が替わるまで通らないもの」が混ざっています。
最初にお伝えしたいのは、上限まわりの失敗は3種類に分かれ、待つ価値があるのは、そのうち1種類だけだということです。見分け方と、その仕分けを書いたコードを書き残します。
「上限」と呼ばれるものは3つある
まず、名前が似ているものを並べて整理します。公式ドキュメントに書かれている内容を、私の言葉で表にしました。
| 種類 | ステータス | 見分ける手がかり | 待てば通るか |
|---|---|---|---|
| 通常のレート制限 | 429 | 多くの場合 retry-after ヘッダが付く | 通る(秒数だけ待つ) |
| 階層の月間上限(Start / Build / Scale) | 429 | error.details.error_code が enforced_spend_limit_reached。retry-after なし | 通らない(翌月 00:00 UTC まで、または階層の引き上げ) |
| 自分で設定した利用上限 | 400 | エラー種別は invalid_request_error。メッセージが You have reached your specified で始まる | 通らない(上限を上げるか外すまで) |
階層ごとの月間上限は、公式の記載では Start が 500 ドル、Build が 1,000 ドル、Scale が 200,000 ドルです。カスタム契約には月間上限がなく、アカウント担当と取り決めます。
ここで厄介なのは、階層の月間上限と通常のレート制限が、どちらも rate_limit_error という同じ種別で返ってくる点です。種別の文字列だけを見て分岐すると、この2つは区別できません。
429 を見るだけでは足りない理由
SDK の自動リトライは、接続エラー・レート制限・5xx を、指数バックオフで既定2回まで再試行します。retry-after があればそれに従います。通常の 429 には、これがちょうど良い振る舞いです。
一方で、階層の月間上限の 429 には retry-after が付かず、公式にも「SDK の自動リトライを含め、アクセスが戻るまで失敗し続ける」と書かれています。待つほど無駄になる、ということです。
さらに、自分で決めた上限の 400 は、そもそもリトライの対象になりません。リクエストの書き方が悪いわけでもないのに invalid_request_error と出るので、プロンプトやパラメータを疑って時間を使いがちです。
補足: Claude Code 用のワークスペースに設けた上限だけは、超えると 400 ではなく、
retry-after付きの 429 になる場合があると公式に書かれています。Claude Code を同じ組織で動かしている方は、この例外も頭の片隅に置いておくと混乱しません。
見分けるコード(標準ライブラリだけ)
何を解決するかを先に言うと、このコードは「このエラーは待つ価値があるか」を1回の関数呼び出しで返します。上限に関係しないエラーなら None を返し、既存の処理に任せます。
"""Anthropic API の「上限」系エラーを仕分ける(標準ライブラリのみ)。"""
from __future__ import annotations
import json
from dataclasses import dataclass
@dataclass
class Verdict:
kind: str # tier_cap / own_limit / rate_limit
retry: bool # 待てば通る可能性があるか
note: str
def classify(status: int, headers: dict[str, str], body: str) -> Verdict | None:
"""上限に関係しないエラーなら None を返す。"""
try:
err = json.loads(body).get("error", {})
except (ValueError, AttributeError):
return None # HTML のエラーページなど、JSON でない応答
message = err.get("message", "")
code = (err.get("details") or {}).get("error_code", "")
h = {k.lower(): v for k, v in headers.items()}
if status == 429 and code == "enforced_spend_limit_reached":
return Verdict("tier_cap", False, "階層の月間上限。翌月 00:00 UTC か階層の引き上げまで通らない")
if status == 400 and message.startswith("You have reached your specified"):
scope = "workspace" if "workspace" in message else "organization"
return Verdict("own_limit", False, f"自分で設定した {scope} の上限。上限を上げるか外すまで通らない")
if status == 429 and "retry-after" in h:
return Verdict("rate_limit", True, f"{h['retry-after']} 秒待てば再試行できる")
if status == 429:
return Verdict("rate_limit", True, "retry-after なし。指数バックオフで様子を見る")
return Noneなぜこう書くかを3点だけ補足します。
- 429 より先に
error_codeを見ています。 種別(rate_limit_error)は通常のレート制限と同じなので、決め手にならないからです。 - 400 はメッセージの書き出しで判定しています。
invalid_request_errorは本物のリクエスト不備にも使われます。公式が示している「You have reached your specified」という書き出しだけを手がかりにして、誤検知を避けました。 - JSON を読めなければ
Noneで返します。 ゲートウェイが返す HTML のエラーページで、仕分け関数自体が落ちるのを防ぐためです。
手元で確かめた結果
ドキュメントに載っている応答の形を真似た疑似の応答を8通り作り、上のコードに通しました。実際の API を叩いたわけではなく、公式の記載どおりの本文を手で組んだものです。
| 入力 | 判定 | 待つか |
|---|---|---|
429 + enforced_spend_limit_reached | tier_cap | 待たない |
| 400 + 「specified API usage limits」 | own_limit(organization) | 待たない |
| 400 + 「specified workspace API usage limits」 | own_limit(workspace) | 待たない |
429 + retry-after: 12 | rate_limit | 12 秒待つ |
| 429(ヘッダなし・通常の本文) | rate_limit | バックオフ |
400(messages: field required) | 上限ではない(None) | — |
| 529 | 上限ではない(None) | — |
| 502 + HTML 本文 | 上限ではない(None) | — |
8件すべて期待どおりに仕分けられました。ただ、本物のメッセージの細部は変わる可能性があります。そのため私は、メッセージ文の一致は「補助の手がかり」にとどめ、error_code を主、ステータスを従として扱っております。
書いてみて気づいたのは、判定そのものよりも、「待たない」と決めたあとに誰へ知らせるかを先に決めておくほうが難しい、ということでした。止めるだけで通知がなければ、止まったことに数日気づかない可能性があるからです。
SDK の例外から使うとき
実際の呼び出しでは、SDK が投げる例外から status_code・ヘッダ・本文を取り出して渡します。
import anthropic
client = anthropic.Anthropic() # ANTHROPIC_API_KEY は環境変数から読まれる
try:
msg = client.messages.create(
model="claude-sonnet-5",
max_tokens=256,
messages=[{"role": "user", "content": "こんにちは"}],
)
except anthropic.APIStatusError as e:
v = classify(e.status_code, dict(e.response.headers), e.response.text)
if v and not v.retry:
# 待っても通らない。再試行せず、人に知らせて止める
print(f"停止: {v.kind} / {v.note} / request_id={e.response.headers.get('request-id')}")
raise SystemExit(1)
raise # それ以外は既存のエラー処理へrequest-id ヘッダは、サポートへ問い合わせるときに必要になります。止めるときに一緒に記録しておくと、後で自分が助かります。
SDK の自動リトライは、階層上限の 429 でも既定どおり2回走ります。止められないのは仕様ですが、2回で終わるので、無駄になるのは短い待ちだけです。待つ価値の有無は、再試行の前に決めます。 これが、このコードで私が引いた線です。
上限に当たったあとの動き方
見分けられたら、次の一手は種類ごとに決まっています。
- 階層の月間上限(429): Console の Rate limits ページで階層を確認し、必要なら「Request tier increase」から引き上げを依頼します。階層を上げれば、アクセスが戻ると公式に書かれています。
- 自分で決めた上限(400): Billing ページの Spend limits で、上限を上げるか外します。ワークスペース単位で決めている場合は、組織全体の上限とは別に当たることがあります。なお、既定のワークスペースには支出上限を設定できません。
- 通常のレート制限(429):
retry-afterの秒数だけ待ちます。急に利用量を増やしたときに出る「加速制限」でも 429 が出るので、トラフィックは少しずつ増やすのが公式の推奨です。
通常のレート制限の余白をヘッダから測る方法は、別の記事に書きました。あわせてご覧ください。
- Claude API が本番で突然 429 を返し始めたとき — レート制限の余白をヘッダから計測して枯渇前に絞る運用メモ
- retry-after が 120 秒の 429 を受けると、Anthropic SDK の自動リトライは 120 秒待とうとしました
次の一歩
まずは、自分の呼び出しコードで 429 を拾っている分岐を1つ探してみてください。そこに error_code の確認を1行足し、400 のメッセージも見張ってみてください。それだけで、月末に「なぜか通らない」と手が止まる夜は、はるかに減ります。
待てるエラーか、待てないエラーかは、再試行の前に決めます。 この一文だけは、疲れている日でも守るようにしています。