What a 529 means, and what it does not
Anthropic documents 529 overloaded_error as "the API is temporarily overloaded", and says it can occur when the API sees high traffic across all users. It is about Anthropic's capacity, not your account. The body has the same shape as every Anthropic error:
{
"type": "error",
"error": { "type": "overloaded_error", "message": "Overloaded" },
"request_id": "req_..."
}Three neighbouring errors look similar and need the opposite response:
| Status and type | Means | Retry? | Fail over? |
|---|---|---|---|
529 overloaded_error | Anthropic is overloaded for everyone | Once, briefly | Yes |
429 rate_limit_error with retry-after | Your organization's rate limit | After retry-after | Yes, if a user is waiting |
429 with error.details.error_code enforced_spend_limit_reached | Your tier's monthly spend cap; no retry-after | No | No |
400 invalid_request_error, "You have reached your specified API usage limits" | A spend limit you set on the organization or workspace | No | No |
500 api_error | An error on Anthropic's side | Once, with backoff | Yes |
A spend limit you set yourself arrives as a 400, not a 429. Code that treats every 400 as a malformed request, or every 429 as a rate limit, gets both of these wrong.
What the SDK does already
Anthropic's SDKs retry transient failures, including 529, twice by default with exponential backoff, honouring retry-after. During a real overload that is three requests to a struggling API before your code sees anything, while the user waits. Where you have somewhere else to send the request, set maxRetries: 0 (max_retries=0 in Python) and decide yourself: one short retry, then fail over.
Rate-limit standing is on every response, in anthropic-ratelimit-requests-*, anthropic-ratelimit-tokens-*, anthropic-ratelimit-input-tokens-* and anthropic-ratelimit-output-tokens-* headers (limit, remaining, reset). They tell you about your limits; they cannot predict a 529.
Retry once, then fail over
This reads the error type, retries a 529 once after a jittered wait, and then answers from OpenAI. It refuses to route around either kind of spend limit.
import Anthropic from "@anthropic-ai/sdk";
import OpenAI from "openai";
// SDK retries off: by default they would retry a 529 twice against the same
// overloaded API before your code sees it. The loop below decides instead.
const anthropic = new Anthropic({ maxRetries: 0, timeout: 30_000 });
const openai = new OpenAI({ maxRetries: 0, timeout: 30_000 });
// The error type Anthropic sent, e.g. "overloaded_error" or "rate_limit_error".
function errorType(err: InstanceType<typeof Anthropic.APIError>): string | undefined {
const body = err.error as { error?: { type?: string } } | undefined;
return err.type ?? body?.error?.type;
}
// A tier spend cap is a 429 that waiting cannot fix.
function isSpendCap(err: InstanceType<typeof Anthropic.APIError>): boolean {
const body = err.error as { error?: { details?: { error_code?: string } } } | undefined;
return body?.error?.details?.error_code === "enforced_spend_limit_reached";
}
export function canFailOver(err: unknown): boolean {
if (err instanceof Anthropic.APIConnectionError) return true; // network or timeout
if (!(err instanceof Anthropic.APIError)) return false;
if (errorType(err) === "overloaded_error") return true; // 529, or mid-stream
if (err.status === 429) return !isSpendCap(err);
// 400 includes a spend limit you set yourself: never route around it.
return err.status !== undefined && err.status >= 500;
}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function answer(prompt: string): Promise<{ text: string; servedBy: string }> {
// One short retry on Claude: a 529 is often brief. Then fail over.
for (let attempt = 0; attempt < 2; attempt++) {
try {
const res = await anthropic.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
});
const text = res.content.map((b) => (b.type === "text" ? b.text : "")).join("");
return { text, servedBy: "anthropic" };
} catch (err) {
if (!canFailOver(err)) throw err;
if (attempt === 0) await sleep(250 + Math.random() * 500);
}
}
const res = await openai.chat.completions.create({
model: "gpt-5.4-mini",
messages: [{ role: "user", content: prompt }],
});
return { text: res.choices[0]?.message.content ?? "", servedBy: "openai" };
}
try {
const { text, servedBy } = await answer("Summarize this support thread in two lines.");
console.log(`[${servedBy}] ${text}`);
} catch (err) {
if (!(err instanceof Anthropic.APIError)) throw err;
console.error(`not retried: ${err.status} ${errorType(err)} (request ${err.requestID})`);
process.exitCode = 1;
}import random
import sys
import time
import anthropic
import openai
# SDK retries off: by default they would retry a 529 twice against the same
# overloaded API before your code sees it. The loop below decides instead.
claude = anthropic.Anthropic(max_retries=0, timeout=30.0)
gpt = openai.OpenAI(max_retries=0, timeout=30.0)
def error_body(err: anthropic.APIStatusError) -> dict:
return (err.body or {}).get("error", {}) if isinstance(err.body, dict) else {}
def can_fail_over(err: Exception) -> bool:
if isinstance(err, anthropic.APIConnectionError): # network or timeout
return True
if not isinstance(err, anthropic.APIStatusError):
return False
body = error_body(err)
if body.get("type") == "overloaded_error": # 529, or mid-stream
return True
if err.status_code == 429:
# A tier spend cap is a 429 that waiting cannot fix.
return body.get("details", {}).get("error_code") != "enforced_spend_limit_reached"
# 400 includes a spend limit you set yourself: never route around it.
return err.status_code >= 500
def answer(prompt: str) -> tuple[str, str]:
# One short retry on Claude: a 529 is often brief. Then fail over.
for attempt in range(2):
try:
res = claude.messages.create(
model="claude-sonnet-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return "".join(b.text for b in res.content if b.type == "text"), "anthropic"
except Exception as err:
if not can_fail_over(err):
raise
if attempt == 0:
time.sleep(0.25 + random.random() * 0.5)
res = gpt.chat.completions.create(
model="gpt-5.4-mini",
messages=[{"role": "user", "content": prompt}],
)
return res.choices[0].message.content or "", "openai"
if __name__ == "__main__":
try:
text, served_by = answer("Summarize this support thread in two lines.")
print(f"[{served_by}] {text}")
except anthropic.APIStatusError as err:
print(f"not retried: {err.status_code} {error_body(err).get('type')} (request {err.request_id})", file=sys.stderr)
sys.exit(1)The overload that arrives mid-stream
With stream: true the response can start with a 200, send some text, and then end with an error event. Anthropic's streaming docs show it in this form:
event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}Both SDKs raise it as an error while you iterate the stream, but there is no HTTP status to read: in TypeScript the error's type is overloaded_error and status is undefined; in Python it is a status error on a 200 response whose body names the type. The rule is the one every streaming failover follows: never continue a half-written answer with another model. Take the partial text back, tell the user, and start again elsewhere.
import Anthropic from "@anthropic-ai/sdk";
import OpenAI from "openai";
const anthropic = new Anthropic({ maxRetries: 0 });
const openai = new OpenAI({ maxRetries: 0 });
// Stand-ins for your UI: show text as it arrives, and take it back.
let onScreen = "";
const show = (text: string) => (onScreen += text);
const retract = () => {
console.log(`partial answer discarded: ${JSON.stringify(onScreen)}`);
onScreen = "";
};
async function streamFromClaude(prompt: string) {
const stream = await anthropic.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: prompt }],
});
// An overloaded_error can arrive here, after the 200 and some text.
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") show(event.delta.text);
}
}
const prompt = "Explain this invoice line by line.";
try {
await streamFromClaude(prompt);
console.log(`[anthropic] ${onScreen}`);
} catch (err) {
// Mid-stream errors have no HTTP status; the type says what happened.
const retryable =
err instanceof Anthropic.APIConnectionError ||
(err instanceof Anthropic.APIError && (err.type === "overloaded_error" || (err.status ?? 0) >= 500));
if (!retryable) throw err;
// Never continue a half-written answer with a different model: the two
// will not join up. Take the partial text back and start again elsewhere.
if (onScreen) retract();
const res = await openai.chat.completions.create({
model: "gpt-5.4-mini",
messages: [{ role: "user", content: prompt }],
});
console.log(`[openai] ${res.choices[0]?.message.content ?? ""}`);
}import json
import anthropic
import openai
claude = anthropic.Anthropic(max_retries=0)
gpt = openai.OpenAI(max_retries=0)
# Stand-ins for your UI: show text as it arrives, and take it back.
on_screen: list[str] = []
def show(text: str) -> None:
on_screen.append(text)
def retract() -> None:
print(f"partial answer discarded: {json.dumps(''.join(on_screen))}")
on_screen.clear()
def stream_from_claude(prompt: str) -> None:
stream = claude.messages.create(
model="claude-sonnet-5-5",
max_tokens=1024,
stream=True,
messages=[{"role": "user", "content": prompt}],
)
# An overloaded_error can arrive here, after the 200 and some text.
for event in stream:
if event.type == "content_block_delta" and event.delta.type == "text_delta":
show(event.delta.text)
if __name__ == "__main__":
prompt = "Explain this invoice line by line."
try:
stream_from_claude(prompt)
print(f"[anthropic] {''.join(on_screen)}")
except (anthropic.APIStatusError, anthropic.APIConnectionError) as err:
# Mid-stream errors arrive on a 200 response; the body's type says what happened.
body = err.body.get("error", {}) if isinstance(getattr(err, "body", None), dict) else {}
status = getattr(err, "status_code", 0)
if isinstance(err, anthropic.APIStatusError) and body.get("type") != "overloaded_error" and status < 500:
raise
# Never continue a half-written answer with a different model: the two
# will not join up. Take the partial text back and start again elsewhere.
if on_screen:
retract()
res = gpt.chat.completions.create(
model="gpt-5.4-mini",
messages=[{"role": "user", "content": prompt}],
)
print(f"[openai] {res.choices[0].message.content}")Knowing it is Anthropic, not you
Anthropic's status page, status.claude.com, lists the Claude API (api.anthropic.com) as its own component and offers Atom and RSS feeds plus email, Slack and webhook subscriptions. Subscribe a webhook or the Atom feed to your incident channel. Your own signal comes first, though: alert when the share of 529 responses (and mid-stream overloaded_error events) for one model passes a few percent over five minutes, and log the request-id header Anthropic returns on every response, which is what their support will ask for.
What to do, case by case
- Retry once, after 0.25–0.75 s
- if a single
529or500arrives and the user can wait a second. - Fail over
- if the retry also fails, or a user-facing request cannot wait.
- Discard and restart elsewhere
- if
overloaded_errorarrives after part of a streamed answer was shown. - Wait
retry-after - if it is a
429 rate_limit_errorthat carries the header. - Stop and alert
- if the 429 carries
enforced_spend_limit_reached, or a400says you reached your usage limits.
Questions
- What does Anthropic error 529 overloaded mean?
- Anthropic's API is temporarily overloaded, which can happen when it sees high traffic across all users. It is not your rate limit or spend limit. Retry once after a short wait, then fail over.
- Is a 529 the same as a 429?
- No. A 429 is your organization's rate limit (or, without retry-after, your tier's spend cap). A 529 is Anthropic's capacity. A spend limit you set yourself returns 400.
- Does the Anthropic SDK retry 529 errors?
- Yes. It retries transient failures, including 529, twice by default with exponential backoff. Set max retries to 0 where you would rather fail over immediately.