You have already implemented proper rate limit handling. 429s are handled gracefully. Then one evening, your Slack bot's error channel starts filling up with a code you have never seen before: 529 Overloaded. That was my experience a few weeks after shipping a production integration, and the instinct to "slow down requests" turned out to be the wrong reflex entirely. The short version: 529 is not an error your application caused. It is a signal that Anthropic's platform itself is temporarily saturated, and the mitigation has to cover how you wait, how you route, and how you degrade.
This article distills the patterns I rely on in production — the ones that actually move the needle when 529 starts showing up in logs.
529 Is Not Your Fault — And That Changes Everything
In Anthropic's error reference, 529 is defined as "The API is temporarily overloaded." The crucial distinction is with its close neighbors. 429 means your own usage has hit the quota assigned to your plan — tokens per minute, requests per minute, or input tokens per day. 503 indicates the service is unavailable for maintenance or unexpected downtime. 529, by contrast, can happen while your own quota still has plenty of headroom; it simply means demand across the platform is outrunning capacity right now.
The practical consequence is that spacing out your requests does not fix 529 the way it fixes 429. The first time I hit 529 in production, my request rate was actually below my normal baseline. Logs showed "type": "overloaded_error" with status code 529. Anthropic's status page confirmed a temporary load spike in the North America region. What brought the service back to healthy was not a smaller request budget, but a combination of smarter retries and a model cascade — both covered below.
Step One: Confirm It Is Actually a 529
Before you start applying fixes, make sure you can distinguish 529 from its neighbors in your logs. If you are catching everything as a generic APIError, you cannot branch retry strategy on error type. With the Python SDK, split them using APIStatusError and its subclasses.
import anthropic
from anthropic import APIStatusError, APIConnectionError
client = anthropic.Anthropic()
def call_with_classification(messages):
try:
return client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=messages,
)
except APIStatusError as e:
err_type = (e.response.json().get("error") or {}).get("type")
print(f"[API {e.status_code}] type={err_type}")
raise
except APIConnectionError as e:
print(f"[Connection] {e}")
raise
# Expected output during an overload: [API 529] type=overloaded_errorFrom there, you branch on err_type — typically overloaded_error vs rate_limit_error — and pick the retry policy that fits. Without that separation, you end up with a blunt "wait longer" strategy that burns time even for errors that would benefit from a completely different approach.
One practical tip: also log the request-id header from the SDK response when you catch an overload. During incidents, Anthropic support can correlate a few of those IDs to regional capacity telemetry much faster than a generic description of symptoms. I keep a tiny helper that extracts request-id, timestamp, model, and input token count into a dedicated overload log file separate from the main application log.
Exponential Backoff With Jitter Is Necessary but Not Sufficient
The baseline for retrying 529 is exponential backoff with randomized jitter. If every client retries at exactly the same interval, they recreate the overload the moment the platform starts recovering. The pattern I use is "Full Jitter," described in detail on the AWS Builders' Library.
import random
import time
from anthropic import APIStatusError
def call_with_full_jitter(fn, max_retries=6, base=1.0, cap=60.0):
"""
Full Jitter retry. Only retries on 429/529/503; leaves 4xx alone.
"""
attempt = 0
while True:
try:
return fn()
except APIStatusError as e:
if e.status_code not in (429, 529, 503):
raise
if attempt >= max_retries:
raise
sleep = random.uniform(0, min(cap, base * (2 ** attempt)))
time.sleep(sleep)
attempt += 1
# Expected behavior: on repeated 529s, wait windows expand from
# up to 1s, 2s, 4s, 8s... while avoiding synchronized retries.Full Jitter alone will pull you through most short overload windows. When the overload is wider or longer, backoff buys time but will not keep a realtime UX acceptable. That is where the next two layers earn their keep.
The Production-Grade Combo: Model Cascade and Regional Diversity
When you cannot make the user wait, give them a slightly lower-quality answer instead of a failure. A model cascade retries with a lighter, faster model if the primary model is overloaded. Sonnet unavailable for a few seconds? Fall back to Haiku for that request.
MODEL_CASCADE = ["claude-sonnet-4-6", "claude-haiku-4-5"]
def resilient_call(messages, max_retries_per_model=3):
last_err = None
for model in MODEL_CASCADE:
try:
return call_with_full_jitter(
lambda: client.messages.create(
model=model,
max_tokens=1024,
messages=messages,
),
max_retries=max_retries_per_model,
)
except APIStatusError as e:
if e.status_code != 529:
raise
last_err = e
continue
raise last_errThe second layer is regional and provider diversity. The Anthropic direct API, Amazon Bedrock, and Google Cloud Vertex AI all host Claude but on independent capacity pools. During real overload events, they rarely degrade in perfect sync — one is often fine while another is saturated. If you run a service with SLA commitments, this is the single highest-leverage insurance policy you can buy. Setup takes some work; my walkthrough for the Bedrock side lives in the Claude API × Amazon Bedrock production guide.
A Circuit Breaker Stops You From Paying for a Broken Path
Retries alone have a failure mode: they keep hammering a degraded endpoint and waste both time and tokens. A circuit breaker trips the sending path open after a burst of failures so you stop sending for a cooldown window. If 5 × 529 show up inside 60 seconds, stop sending to the direct API for 120 seconds and route only to the fallback path.
import time
from collections import deque
from threading import Lock
class CircuitBreaker:
def __init__(self, failure_threshold=5, window_sec=60, open_sec=120):
self.failures = deque()
self.window = window_sec
self.threshold = failure_threshold
self.open_sec = open_sec
self.opened_at = None
self.lock = Lock()
def allow(self):
with self.lock:
now = time.time()
if self.opened_at and now - self.opened_at < self.open_sec:
return False
if self.opened_at:
self.opened_at = None
return True
def record_failure(self):
with self.lock:
now = time.time()
self.failures.append(now)
while self.failures and now - self.failures[0] > self.window:
self.failures.popleft()
if len(self.failures) >= self.threshold:
self.opened_at = now
# Expected behavior: after 5 × 529 within a 60s window, allow() returns
# False for 120s, and upstream code routes to the fallback path.Libraries like pybreaker in Python or opossum in Node.js offer more features, but the 30-line version above is often enough for early production. Emit a metric each time the breaker opens; that single graph will tell you more about Anthropic's platform health than most monitoring setups I have seen.
One subtlety worth noting: you want separate breaker instances per upstream, not a single global one. If the direct Anthropic API is overloaded but Bedrock is healthy, a global breaker would incorrectly block traffic to Bedrock as well. Keying breakers by (provider, region) keeps each path independent, which is exactly the property that makes the fallback design pay off during a real incident.
Non-Code Levers: Priority Tier and Batches API
Two more levers are worth remembering because they live outside your application code.
The first is Anthropic's Priority Tier on paid plans. Higher tiers are prioritized when the platform is under pressure, which reduces how often 529 reaches your traffic at all. For B2B workloads with strict SLAs, the cost can be justified directly from availability numbers. I cover the implementation details in the Claude API production rate limit & billing guide.
The second is the Batches API. Any workload that does not need sub-second latency — nightly summaries, large analytical passes, offline enrichment — should go through Batches rather than synchronous requests. That removes load from your synchronous path and, by extension, reduces your exposure to 529. The practical migration playbook is in the Claude API Batches async processing guide.
The Single Step You Can Take Today
The right goal for 529 is not to prevent it — you cannot — but to keep the user experience intact when it arrives. Before you touch anything else, check whether Full Jitter retries are in your production code path. If they are not, adding the thirty lines above silently absorbs more than half of the 529s you were seeing. From there, add a circuit breaker and a model cascade and you will have a service that stays useful even when the platform is having a bad afternoon.