MemberPress AI Academy memberpress.ai

Production · Production Basics

Errors, retries, and timeouts

Video15 min

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: 400 is not retryable. Add a guard that only retries on 429, 529, and APITimeoutError. Retrying a 400 wastes quota and time.
  • The backoff delay is not increasing between retries: confirm delay *= 2 runs after time.sleep, not before. If it is at the wrong indentation level it may reset each iteration.
  • APIStatusError is not catching 429: use anthropic.RateLimitError for 429 specifically — it is a subclass with its own except clause. Catching APIStatusError alone will catch it, but then you need to inspect e.status_code to distinguish 429 from other status codes.
  • You do not see the Retry-After header: the SDK surfaces it on the exception object. Print e.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.