Handle Claude API 429 and 529 Errors in a Python Agent Loop (2026)
A 429 is your rate limit; a 529 is Anthropic overloaded. Catch both, honor retry-after, back off with jitter, cap the retries, and never retry a 400. Full runnable Python wrapper for agent loops.

On this page
Quick Answer (2026)
A 429 from the Claude API is a rate_limit_error: you sent more requests or tokens than your account's limit allows, and the response includes a retry-after header telling you how long to wait. A 529 is an overloaded_error: Anthropic's API is temporarily saturated across all traffic, it is not your fault, and there is no retry-after header. Both are transient, so both should be retried, but you must catch them differently from client errors like 400 or 401, which you should never retry. The fix is one small wrapper: catch the retryable statuses, honor retry-after when present, otherwise back off exponentially with jitter, and cap the number of attempts.
If you run agents against the Claude API in
Python, a long tool-use loop will eventually hit a
429 or a 529. A naive try/except: sleep(1) makes it worse. Here is the loop that actually holds up.
What the two errors actually mean
The Anthropic API returns a typed error body. The two you retry most often are:
{"type": "error", "error": {"type": "rate_limit_error", "message": "..."}}
{"type": "error", "error": {"type": "overloaded_error", "message": "..."}}
429 rate_limit_error is about you. Your organization has requests-per-minute, input-tokens-per-minute, and output-tokens-per-minute ceilings tied to your usage tier. Cross any of them and you get a 429 with a retry-after header measured in seconds. The lever is to slow down and spread load. See Anthropic's errors and rate-limits reference (2026) for the current headers and tier behavior.
529 overloaded_error is about the API. It means Anthropic's servers are momentarily saturated for everyone, independent of your limits. There is no retry-after header because there is no per-account number to hand back. It usually clears within seconds. The lever is to wait and try again, politely, without stampeding.
Every other provider has the same split. OpenAI, for example, separates per-account rate limits (2026) from server-side 503 overloads. The handling pattern below is portable across both.
See it raised
In the Anthropic Python SDK, these arrive as typed exceptions. RateLimitError is a subclass of APIStatusError, and so are the 5xx errors, which means you can catch the whole retryable family in one place and branch on status_code.
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
try:
msg = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Summarize this ticket."}],
)
except anthropic.RateLimitError as err:
print("429:", err.status_code) # you are over your limit
except anthropic.APIStatusError as err:
print("other HTTP error:", err.status_code) # 529, 500, 400, ...
The wrong fix
This is the version that ships in a hurry and pages you at 2am:
# DO NOT do this
while True:
try:
return client.messages.create(model=MODEL, max_tokens=1024, messages=messages)
except Exception:
time.sleep(1) # retries forever, no jitter, retries 400s too
Three problems. It retries a 400 invalid_request_error forever, so a real bug becomes an infinite loop. It uses a fixed one-second sleep, so during a 529 every client you run reconnects on the same beat, a thundering herd that keeps the API overloaded. And it never gives up, so a stuck call blocks the whole agent.
The right fix: honor retry-after, then full jitter
Two rules. When the server tells you how long to wait with retry-after, trust it. When it does not, use exponential backoff with full jitter, sleeping a random amount between zero and a cap that doubles each attempt. Full jitter is the strategy AWS recommends in Exponential Backoff And Jitter; it spreads recovering clients out so they stop synchronizing.
import random
import time
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-sonnet-5"
# Transient. Everything else (400, 401, 403, 404, 413, 422) is your request, not a blip.
RETRYABLE = {408, 409, 429, 500, 502, 503, 529}
def _delay(err, attempt, base, cap):
# 429 carries retry-after (seconds). Trust the server when it speaks.
retry_after = err.response.headers.get("retry-after")
if retry_after:
try:
return float(retry_after)
except ValueError:
pass
# 529 and 5xx have no retry-after: full jitter over an exponentially growing cap.
ceiling = min(cap, base * (2 ** attempt))
return random.uniform(0, ceiling)
def call_claude(messages, *, max_retries=6, base=1.0, cap=60.0, **kwargs):
attempt = 0
while True:
try:
return client.messages.create(
model=MODEL, max_tokens=1024, messages=messages, **kwargs
)
except anthropic.APIStatusError as err:
if err.status_code not in RETRYABLE or attempt >= max_retries:
raise # non-transient, or we are out of attempts: fail loudly
wait = _delay(err, attempt, base, cap)
attempt += 1
print(f"{err.status_code}, retry {attempt}/{max_retries} in {wait:.1f}s")
time.sleep(wait)
except (anthropic.APIConnectionError, anthropic.APITimeoutError):
if attempt >= max_retries:
raise
wait = random.uniform(0, min(cap, base * (2 ** attempt)))
attempt += 1
time.sleep(wait)
RateLimitError (429) and InternalServerError (5xx) both flow through the single APIStatusError branch, so 529 is handled with no extra case. Network drops and timeouts get their own branch because they have no HTTP status to inspect. The raise on a non-retryable status is the important line: a 400 fails immediately instead of looping.
Reach for the built-in first
Before you hand-roll anything, know that the SDK already does most of this. The client retries a couple of times by default on 408, 409, 429, and 5xx, with backoff, honoring retry-after. Raise the ceiling in one line:
client = anthropic.Anthropic(max_retries=5)
So when do you still need the wrapper above? When one logical unit of work makes many calls and must respect a budget the SDK cannot see: a multi-step orchestrator-workers agent loop that fans out dozens of sub-calls, a streaming run you want to resume, or any loop where you cap total wall-clock time rather than per-call attempts. In those cases you want the retry accounting at the loop level, not buried per request.
Verify it without waiting for a real outage
You should not have to trigger a real 529 to trust your loop. Inject a fake client that fails a set number of times, then succeeds, and assert the loop recovered:
class FlakyClient:
def __init__(self, fail_times, status):
self.left, self.status = fail_times, status
self.messages = self
def create(self, **_):
if self.left > 0:
self.left -= 1
raise anthropic.APIStatusError(
"overloaded",
response=_fake_response(self.status),
body=None,
)
return {"ok": True}
# Point call_claude at FlakyClient(fail_times=3, status=529),
# set base=0.01 to keep the test fast, and assert it returns {"ok": True}
# after exactly 3 retries. Then set status=400 and assert it raises immediately.
The two assertions that matter: a 529 recovers within your retry budget, and a 400 never retries. If both hold, the loop is correct. For the exact retry_after and error-type fields, keep the Anthropic errors reference (2026) open while you test.
A retry loop is close cousin to a graph that will not terminate. If your agent runs on LangGraph and you are also fighting a GraphRecursionError, that is a different ceiling with a different fix, covered in the LangGraph recursion limit tutorial.
Written by
Ren OkabeRen Okabe builds and debugs LLM agent systems, with a focus on LangGraph orchestration, evals, and production reliability.
Frequently asked questions
What is the difference between a Claude API 429 and a 529 error?
A 429 is a rate_limit_error: your account exceeded its requests-per-minute or tokens-per-minute limit, so it is specific to you and the response carries a retry-after header. A 529 is an overloaded_error: Anthropic's API is temporarily saturated across all traffic, it is not your fault, and there is no retry-after header. Both are transient and both should be retried, but 429 means slow down while 529 means wait and try again.
Does the Anthropic Python SDK retry rate limit errors automatically?
Yes. The client retries a small number of times by default on 408, 409, 429, and 5xx responses with exponential backoff, and it honors the retry-after header. You raise the ceiling with anthropic.Anthropic(max_retries=5). You still need your own loop when a single agent run makes many calls, streams, or must respect a wall-clock or token budget the SDK does not know about.
Should I retry a 400 or 401 from the Claude API?
No. A 400 invalid_request_error, 401 authentication_error, 403 permission_error, and 404 not_found_error are caused by your request, not by a transient blip. Retrying them just burns time and quota while failing the same way. Only retry 408, 409, 429, and 5xx including 529.
What backoff strategy should I use for 529 overloaded errors?
Exponential backoff with full jitter: sleep a random amount between zero and a cap that doubles each attempt. Jitter spreads retries out so many clients recovering from the same overload do not all hammer the API at the same instant, which is the thundering-herd problem. Cap the delay, cap the number of retries, and give up cleanly after that.
Related tutorials
LangGraph Recursion Limit: Fix GraphRecursionError Properly (2026)
GraphRecursionError means you hit LangGraph's 25-superstep recursion limit. Raise it the right way, find the real infinite loop, and exit gracefully with RemainingSteps.
Orchestrator-Workers Pattern in Python: A Claude Tutorial (2026)
Build the orchestrator-workers agent pattern in Python with Claude: a planner that decides subtasks at runtime, parallel workers, and a synthesizer. Runnable code, July 2026.
