Production · Production Basics
Errors, retries, and timeouts
Every production API integration eventually hits an error. The question is whether your code handles it gracefully or whether it surfaces a stack trace to the user.
The five error classes
The API returns standard HTTP status codes. Each has a distinct cause and the right response to it.
400 invalid_request_error — the request body is malformed or missing a required field. This is a bug in your code, not a transient condition. Do not retry. Fix the request.
401 authentication_error — the API key is wrong, missing, or expired. Do not retry. Fix the key.
403 permission_error — the key is valid but cannot access the requested model or feature. Check the console for model access. Do not retry.
429 rate_limit_error — you have exceeded your token-per-minute or request-per-minute quota. Retry with backoff after the wait period indicated in the Retry-After header.
529 overloaded_error — Anthropic's servers are temporarily at capacity. This is not your fault. Retry with backoff.
429 versus 529 — different causes, same response
A 429 means you are moving too fast relative to your tier's limits. A 529 means Anthropic's infrastructure is temporarily saturated. Both errors respond to exponential backoff, but the 529 sometimes needs a longer initial wait before the first retry.
Check the response body to confirm which error you have. Both return JSON with {"type": "error", "error": {"type": "rate_limit_error", ...}} or "overloaded_error".
Exponential backoff with jitter
# scripts/retry.py
import anthropic, time, random
client = anthropic.Anthropic()
def call_with_retry(messages, max_retries=3):
delay = 1.0
for attempt in range(max_retries + 1):
try:
return client.messages.create(
model="claude-opus-4-5",
max_tokens=256,
messages=messages,
)
except anthropic.RateLimitError as e:
if attempt == max_retries:
raise
jitter = random.uniform(0, delay * 0.1)
print(f"429 rate limit, retry {attempt + 1}/{max_retries}, waiting {delay + jitter:.1f}s", flush=True)
time.sleep(delay + jitter)
delay *= 2
except anthropic.APIStatusError as e:
if e.status_code == 529 and attempt < max_retries:
jitter = random.uniform(0, delay * 0.1)
print(f"529 overloaded, retry {attempt + 1}/{max_retries}, waiting {delay + jitter:.1f}s", flush=True)
time.sleep(delay + jitter)
delay *= 2
else:
raise
result = call_with_retry([{"role": "user", "content": "Say hello."}])
print(result.content[0].text)
The jitter — random.uniform(0, delay * 0.1) — prevents a stampede when many clients fail at the same moment and all wake up at the same time. Without jitter, they hit the API simultaneously and trigger another 429.
Timeouts — the silent failure
The Anthropic SDK does not set a default request timeout. A long-running request can hang indefinitely if the connection is dropped silently. Set a timeout explicitly:
client = anthropic.Anthropic(timeout=30.0) # seconds
On timeout, the SDK raises anthropic.APITimeoutError. Treat it like a 529 — retryable with backoff. Add it to the except chain:
except anthropic.APITimeoutError:
if attempt < max_retries:
time.sleep(delay)
delay *= 2
else:
raise
What errors to surface to users
Never surface raw error messages from the API to end users. Map them: 429 or 529 becomes "The assistant is busy — try again in a moment." A 400 or 401 means something is broken in your backend — return 500 to the caller and log the real error server-side.
If you get stuck
- Your retry loop retries a
400:400is not retryable. Add a guard that only retries on429,529, andAPITimeoutError. Retrying a400wastes quota and time. - The backoff delay is not increasing between retries: confirm
delay *= 2runs aftertime.sleep, not before. If it is at the wrong indentation level it may reset each iteration. APIStatusErroris not catching429: useanthropic.RateLimitErrorfor429specifically — it is a subclass with its own except clause. CatchingAPIStatusErroralone will catch it, but then you need to inspecte.status_codeto distinguish429from other status codes.- You do not see the
Retry-Afterheader: the SDK surfaces it on the exception object. Printe.response.headers.get("retry-after")to read it.
Cross-reference
l5 (Rate limits and the cost of context) explains why you hit 429 — understanding your tier limits gives you the right ceiling to build against. l3 (Streaming) mentions what happens when a streaming connection drops mid-response; wrap streaming calls in the same retry logic.
Deliverable
A call_with_retry function that handles 429 and 529 with exponential backoff and jitter, logs each retry attempt to stderr, and raises after three failed attempts. Test it by temporarily setting a wrong key to trigger 401, and confirm the function does not retry on that error class.
Try these prompts
- Write a Python retry function that handles 429 and 529 errors with exponential backoff and jitter, but does not retry on 400 or 401.
- What is the difference between a 429 and a 529 error from the Anthropic API? How should your code handle each one differently?
- Write the except block that catches APITimeoutError and retries with the same backoff logic as a 529.
Sign in to keep your place and tick off lessons. Reading stays open either way.