AI API 429, 529 and 503 Errors: Retry Rules That Match the Docs

TL;DR
Which 429, 529 and 503 errors to retry across OpenAI, Claude, Gemini and Bedrock, how to read Retry-After, and a typed wrapper that skips quota errors.
Retry rate limits (429) and provider overloads (Anthropic and Bedrock 529, OpenAI, Gemini, DeepSeek and Mistral 503) with exponential backoff plus jitter, and treat Retry-After as the minimum wait. Never retry the 429s and 402s that mean money ran out: OpenAI's credit_balance_exhausted and spend or usage limit codes, Anthropic's enforced_spend_limit_reached, and Gemini's 402. Gemini's daily quota_exceeded is a separate case: wait for the reset or request more quota, but do not loop on it. The official OpenAI and Anthropic SDKs already retry twice by default, but they decide on status code alone, and @google/genai does not retry generateContent at all unless you opt in.
This guide maps each provider's documented error semantics, the headers worth reading, what each SDK does on its own, and a type-checked TypeScript wrapper that encodes the rules. For the numbers behind each 429, see the per-provider tables on /rate-limits. Every error code in this post, with its documented retry guidance, is also in the AI API error reference, alongside the coding agent error reference.
Last updated: October 18, 2026
The three kinds of "too many" error#
Every provider in this guide returns some flavour of "not now", but the docs describe three different situations, and only two of them get better with time.
Rate limit: your per-minute budget. You sent more requests or tokens than your tier allows in a window. It is transient and specific to your account. OpenAI returns 429 rate_limit_error (including the newer slow_down code for traffic that ramps too fast), Anthropic returns 429 rate_limit_error, Gemini returns 429 RESOURCE_EXHAUSTED on generateContent or rate_limit_exceeded / too_many_requests on the Interactions API, and Bedrock returns 429 ThrottlingException. Wait, then retry.
Overload or capacity: the provider's problem. The service is temporarily short on capacity, regardless of your quota. Anthropic returns 529 overloaded_error. OpenAI returns 503 with code server_is_overloaded. Gemini returns 503 UNAVAILABLE when the service is temporarily running low on capacity or is overloaded. Bedrock returns 503 ServiceUnavailable, which AWS says is "not related to your account-level quotas", and separately documents a 529 overloaded_error for high demand or insufficient serving capacity. Groq has a custom 498 for Flex Tier capacity. Backing off helps, and so does switching model or Region.
Quota or billing: a wall. Money, credits or a hard cap ran out. Retrying cannot fix it, and some of these arrive as 429 with the same type as a real rate limit. OpenAI's docs are blunt: "Retrying billing, spend, or quota errors won't restore API access. Update the relevant credits or limits before sending another request." Anthropic says the same about its tier spend cap (enforced_spend_limit_reached): "Retrying, including the SDK's automatic retries, fails until access resumes."
%% title: Retry or stop
flowchart TD
A[Error response] --> B{Status?}
B -->|429| C{Error code says quota or billing?}
C -->|Yes| D[Stop and alert]
C -->|No| E{Retry-After present?}
E -->|Yes| F[Wait at least that long plus jitter]
E -->|No| G[Exponential backoff with jitter]
B -->|529 / 503 / 500| G
B -->|402| D
B -->|Other 4xx| H[Stop: fix the request]
F --> I[Retry]
G --> IThe practical consequence: a status-code check is not enough. You have to read the error body.
Cross-provider table#
Wording below stays close to each provider's own error page, read on October 18, 2026. A dash means the docs say nothing specific.
| Provider | Rate-limit signal | Overload signal | Do-not-retry signal | Retry-After documented? |
|---|---|---|---|---|
| OpenAI | 429 rate_limit_error, 429 slow_down | 503 service_unavailable_error / server_is_overloaded | 429 with credit_balance_exhausted, organization_spend_limit_exceeded, project_spend_limit_exceeded, organization_usage_limit_exceeded (type may be insufficient_quota) | Yes, on temporary 429s and 503 overloads, when present |
| Anthropic | 429 rate_limit_error | 529 overloaded_error | 429 with error.details.error_code = enforced_spend_limit_reached; 400 for a spend limit you set yourself (limits on the Claude Code workspace can return a 429 with retry-after instead); 402 billing_error | Yes, but not on every 429; never on the tier spend-cap 429 |
| Gemini | 429 RESOURCE_EXHAUSTED (generateContent); 429 rate_limit_exceeded, too_many_requests (Interactions) | 503 UNAVAILABLE / service_unavailable | 402 (Prepay credits depleted: "Don't retry"); 429 quota_exceeded is a daily quota, wait for reset | Not on the error pages |
| Bedrock | 429 ThrottlingException | 503 ServiceUnavailable; 529 overloaded_error | Sustained throttling means a quota increase, Reserved Tier or Provisioned Throughput | The 529 entry says honour Retry-After if present |
| Mistral | 429 | 500, 502, 503, 504 | 402 Payment required is listed outside the transient set | - (glossary says exponential backoff for 429 and 5xx) |
| xAI | 429 | - | - | - (reduce rate or raise the limit in xAI Console) |
| DeepSeek | 429 Rate Limit Reached | 503 Server Overloaded | 402 Insufficient Balance (top up) | - |
| Groq | 429 | 498 Flex Tier Capacity Exceeded; 500, 502, 503 | - | Yes, retry-after on 429 only |
The do-not-retry codes each have a page in the error reference with the provider's own wording: OpenAI's credit_balance_exhausted, organization_spend_limit_exceeded, project_spend_limit_exceeded and organization_usage_limit_exceeded; Anthropic's 402 billing_error; Gemini's 402 and daily quota_exceeded; and DeepSeek's 402 Insufficient Balance.
Three details from that table catch people out.
First, OpenAI moved the slow_down signal. Its rate-limits guide notes that rapid traffic increases now return 429 slow_down where some endpoints used to return 503, while model overload stays 503 server_is_overloaded, and video overload now returns 503 where it used to return 429. The guide says to handle both RateLimitError (429) and InternalServerError (503). It also gives a ramp rule: once traffic reaches 1 million input TPM, grow it by no more than 50% every 15 minutes, because slow_down can fire even when you are inside your RPM and TPM limits.
Second, OpenAI's quota errors share a status with rate limits. The docs say: "For billing-related errors, inspect error.code to identify the specific cause. The broader error.type can still be insufficient_quota."
Third, Anthropic has two different spend-limit responses. A spend limit you set yourself returns 400 invalid_request_error with a message beginning "You have reached your specified API usage limits" (see the 400 spend-limit page). The one documented exception is the Claude Code workspace: its limits are checked separately, and Claude Code requests over that workspace's limit can instead receive a 429 that carries a retry-after header. The wrapper below treats that 429 like any other rate limit: it honours retry-after, and gives up when the wait is longer than maxRetryAfterMs. The usage tier's monthly spend cap returns 429 rate_limit_error with no retry-after header and error.details.error_code set to enforced_spend_limit_reached. Usage then pauses until 00:00 UTC on the first of the next month, unless you move to a higher tier or request a higher limit sooner; the rate-limits page notes that moving to a higher tier restores access. Either way, waiting a few seconds does nothing, so if your code treats every Anthropic 429 as "wait and retry", that cap turns into a retry storm against a wall that only a tier or limit change, or the calendar, will move. Our earlier Claude API reliability guide predates that distinction; this post is the corrected rule.
Read the headers before you guess#
Most providers tell you how much budget is left. Reading it lets you slow down before the 429, not after.
OpenAI returns x-ratelimit-limit-requests, x-ratelimit-limit-tokens, x-ratelimit-remaining-requests, x-ratelimit-remaining-tokens, x-ratelimit-reset-requests and x-ratelimit-reset-tokens, plus project-scoped x-ratelimit-limit-project-tokens, x-ratelimit-remaining-project-tokens and x-ratelimit-reset-project-tokens. Reset values are durations like 1s or 6m0s, not timestamps. Retry-After is the minimum number of seconds to wait on a temporary rate-limit error when it is present.
Anthropic returns anthropic-ratelimit-requests-*, anthropic-ratelimit-tokens-*, anthropic-ratelimit-input-tokens-* and anthropic-ratelimit-output-tokens-* (each with -limit, -remaining, -reset), plus anthropic-priority-* for Priority Tier. Resets are RFC 3339 timestamps and the token -remaining values are rounded to the nearest thousand. The docs describe retry-after as the seconds to wait, adding that earlier retries will fail, and note that not every 429 carries it.
Groq uses the same x-ratelimit-* names as OpenAI with a twist that is easy to misread: x-ratelimit-*-requests always refers to requests per day, and x-ratelimit-*-tokens always refers to tokens per minute. A low remaining-requests value on Groq is a daily budget, not a per-minute one. retry-after is only set on 429.
AWS documents a different header at the SDK level: some services send x-amz-retry-after, a delay in milliseconds, which the SDKs use under the opt-in 2026 retry behavior (more on that in the SDK table below). The Bedrock error page's 529 entry, by contrast, refers to the standard Retry-After.
One more reason not to hammer: OpenAI's rate-limits guide states that unsuccessful requests contribute to your per-minute limit. A tight retry loop spends the budget you are waiting to get back.
%% title: Honouring Retry-After
sequenceDiagram
participant App
participant API
App->>API: POST request
API-->>App: 429 + retry-after: 20
App->>App: Wait 20s + small jitter
App->>API: Retry
API-->>App: 200 OKOpenAI's guide adds two rules worth copying into your design doc: treat Retry-After as a minimum and add a small random delay, and if a valid server delay is longer than your maximum, stop and defer the work instead of retrying sooner. It also notes that Retry-After on a 429 or 503 does not mean quota or billing errors can be fixed by retrying, and that how an SDK handles Retry-After varies by SDK version and configuration.
What the official SDKs already do#
Before you write a retry loop, know that one probably already runs inside your client. Versions below were checked on npm on October 18, 2026, by reading each package's published source. The AWS rows come from the AWS SDKs retry behavior page and AWS's announcement of the updated behavior. That page opens with a caveat that matters: everything it describes "requires opting in until it becomes the default behavior" by setting AWS_NEW_RETRIES_2026=true, and without that flag "your SDK uses pre-2026 retry behavior, which differs in backoff timing, retry quota costs, and service-specific defaults." AWS says the new behavior becomes the default in November 2026, so the table splits the two.
| SDK | Default retries | Statuses retried | Backoff | Retry-After |
|---|---|---|---|---|
openai@7.38.0 | maxRetries: 2 | x-should-retry header if sent, else 408, 409, 429, 500+ | 0.5s doubling to 8s, up to 25% jitter | Reads retry-after-ms and retry-after; a delay over 60s is not used and the default backoff applies instead |
@anthropic-ai/sdk@0.135.0 | maxRetries: 2 | Same rules, so 529 is included | 0.5s to 8s, up to 25% jitter | Reads both headers and honours any positive delay (no 60s cap) |
@google/genai@2.30.0, models.generateContent | None unless httpOptions.retryOptions is set | When opted in: 408, 429, 500, 502, 503, 504 | Opt-in defaults: 5 attempts including the first, 1s initial, 60s max, base 2, jitter on | Not read on this path |
@google/genai@2.30.0, interactions.create | Up to 4 retries | 408, 409, 429, 5XX, plus connection errors | 500 ms initial, exponent 2, 8s max interval | Reads retry-after-ms / retry-after but caps the wait at 8s |
| AWS SDKs (Bedrock), default behavior today | Varies by SDK: standard mode in most, legacy in Java, Python, Ruby, PHP, C++ and the AWS CLI; some SDKs default to more than 3 attempts | Throttling and transient errors; legacy sets vary by SDK | One base delay for every error type, anywhere from 10 ms to 1,000 ms depending on the SDK | Not standardized; your SDK's GitHub tracking issue (linked from the AWS page) lists its before-and-after values |
AWS SDKs, 2026 retry behavior (opt-in: AWS_NEW_RETRIES_2026=true) | standard mode, 3 attempts total | Throttling and transient errors | random(0,1) x min(20s, base x 2^retry), base 1s for throttling, 50 ms for transient | Uses x-amz-retry-after (ms) when a service sends it, clamped between the computed backoff and backoff plus 5s (25s max), with no added jitter. Standard Retry-After is not mentioned, so honour the Bedrock 529 Retry-After yourself. adaptive mode adds a client-side rate limiter |
| Mistral Python and TypeScript SDKs | Built in, per Mistral's docs | Not specified in the glossary | Exponential backoff | Not specified |
The OpenAI and Anthropic SDKs get a lot right for transient failures: jittered exponential backoff, server-provided delays, and an x-should-retry escape hatch that lets the server veto a retry. Anthropic's own errors page confirms its SDK retries connection errors, rate limits and 5xx twice by default while honouring retry-after. The AWS SDKs go further with a client-side retry quota and an adaptive mode that rate-limits before the service has to. For the common case of a short burst of 429s or 529s, the defaults are reasonable and you may not need anything else.
The gaps are specific:
- Retry decisions are status-based. Neither the OpenAI nor the Anthropic SDK inspects
error.codebefore retrying, so an OpenAIcredit_balance_exhausted429 or an Anthropic spend-cap 429 uses up its retries before surfacing, unless the server sendsx-should-retry: false. That adds latency and two wasted requests per call, which matters most for interactive requests and batch jobs that fan out. - Long server delays need your own logic in openai-node. If
Retry-Aftersays 90 seconds,openai@7.38.0falls back to its own 0.5s to 8s schedule. OpenAI's guide recommends deferring the work when the server delay exceeds your cap, so long waits need handling outside the SDK. - Gemini's
generateContentpath does not retry by default. If you call it bare, the first 429 or 503 reaches your code. - With the 2026 retry behavior enabled, the AWS SDKs cap server-directed waits at 25 seconds, and the AWS retry behavior page does not mention the standard
Retry-Afterheader, so a long Bedrock 529Retry-Afteris something your code has to read. Without the opt-in flag, backoff timing depends on your SDK's pre-2026 defaults.
If you run Claude Code rather than the SDK, the CLI has its own retry loop and an opt-in watchdog for unattended runs, covered in the watchdog guide linked under Continue Reading.
Nested retries multiply requests#
The most common bug is running your own loop on top of SDK retries without noticing.
%% title: Nested retry multiplication
flowchart LR
A[App loop: 5 attempts] --> B[SDK call]
B --> C[SDK: 1 try + 2 retries]
C --> D[Up to 15 HTTP requests]
D --> E[Each failure counts against your limit]OpenAI's guide says it directly: disable SDK retries or account for them so nested loops do not multiply requests. The wrapper below sets maxRetries: 0 on the clients for exactly this reason.
A typed retry wrapper (TypeScript)#
This wrapper does five things the SDK defaults do not: it refuses to retry quota and billing errors on the first attempt, it treats Retry-After as a floor rather than a suggestion, it gives up (instead of retrying early) when the server asks for a wait longer than you can afford, it enforces an overall time budget as well as an attempt cap, and it uses one policy across all three SDKs.
npm i openai@7.38.0 @anthropic-ai/sdk@0.135.0 @google/genai@2.30.0
npm i -D typescript @types/node// retry.ts - one retry policy for OpenAI, Anthropic and Gemini SDK errors.
import OpenAI from "openai";
import Anthropic from "@anthropic-ai/sdk";
import { ApiError as GeminiApiError } from "@google/genai";
export type RetryDecision =
| { retry: true; retryAfterMs?: number; reason: string }
| { retry: false; reason: string };
// OpenAI 429 codes that mean "fix billing or limits", not "wait".
const OPENAI_NO_RETRY_CODES = new Set([
"insufficient_quota",
"credit_balance_exhausted",
"organization_spend_limit_exceeded",
"project_spend_limit_exceeded",
"organization_usage_limit_exceeded",
]);
// Retry-After is seconds or an HTTP date; retry-after-ms is milliseconds.
export function parseRetryAfter(headers: Headers | undefined): number | undefined {
if (!headers) return undefined;
const ms = headers.get("retry-after-ms");
if (ms !== null && Number.isFinite(Number(ms)) && Number(ms) >= 0) return Number(ms);
const value = headers.get("retry-after");
if (value === null) return undefined;
const seconds = Number(value);
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
const date = Date.parse(value);
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
return undefined;
}
// Status-based fallback for errors that fail the instanceof checks, for example
// when a mixed CJS/ESM build loads two copies of an SDK.
function classifyByShape(err: unknown): RetryDecision | undefined {
if (typeof err !== "object" || err === null) return undefined;
const e = err as {
status?: unknown;
code?: unknown;
type?: unknown;
headers?: unknown;
error?: { error?: { details?: { error_code?: unknown } } };
};
if (typeof e.status !== "number") return undefined;
const code = typeof e.code === "string" ? e.code : "";
const headers = e.headers instanceof Headers ? e.headers : undefined;
const spendCap = e.error?.error?.details?.error_code === "enforced_spend_limit_reached";
if (e.status === 429 && (OPENAI_NO_RETRY_CODES.has(code) || e.type === "insufficient_quota" || spendCap)) {
return { retry: false, reason: `quota ${code || "spend cap"}` };
}
if (e.status === 429 || e.status >= 500) {
return { retry: true, retryAfterMs: parseRetryAfter(headers), reason: `status ${e.status}` };
}
return { retry: false, reason: `status ${e.status}` };
}
export function classify(err: unknown): RetryDecision {
// Network failures and timeouts carry no status: retry them.
if (err instanceof OpenAI.APIConnectionError || err instanceof Anthropic.APIConnectionError) {
return { retry: true, reason: "connection" };
}
if (err instanceof OpenAI.APIError) {
const code = err.code ?? "";
if (err.status === 429 && (OPENAI_NO_RETRY_CODES.has(code) || err.type === "insufficient_quota")) {
return { retry: false, reason: `openai ${code || err.type}` };
}
if (err.status === 429 || (err.status ?? 0) >= 500) {
return { retry: true, retryAfterMs: parseRetryAfter(err.headers), reason: `openai ${err.status}` };
}
return { retry: false, reason: `openai ${err.status}` };
}
if (err instanceof Anthropic.APIError) {
const body = err.error as { error?: { details?: { error_code?: string } } } | undefined;
if (err.status === 429 && body?.error?.details?.error_code === "enforced_spend_limit_reached") {
return { retry: false, reason: "anthropic spend cap" };
}
if (err.status === 429 || (err.status ?? 0) >= 500) {
// 529 overloaded_error falls in the >= 500 branch.
return { retry: true, retryAfterMs: parseRetryAfter(err.headers), reason: `anthropic ${err.status}` };
}
return { retry: false, reason: `anthropic ${err.status}` };
}
if (err instanceof GeminiApiError) {
// 402 means Prepay credits are gone. Gemini errors expose no headers here.
if ([429, 500, 503].includes(err.status)) {
return { retry: true, reason: `gemini ${err.status}` };
}
return { retry: false, reason: `gemini ${err.status}` };
}
return classifyByShape(err) ?? { retry: false, reason: "unknown error" };
}
export interface RetryOptions {
maxAttempts?: number; // total attempts, including the first
baseDelayMs?: number;
maxDelayMs?: number; // cap for computed backoff
maxRetryAfterMs?: number; // longest server-requested wait we accept
deadlineMs?: number; // total time budget across all attempts and waits
signal?: AbortSignal;
onRetry?: (info: { attempt: number; delayMs: number; reason: string }) => void;
}
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(signal.reason);
const timer = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(timer);
reject(signal?.reason);
};
signal?.addEventListener("abort", onAbort, { once: true });
});
}
export async function withRetry<T>(fn: () => Promise<T>, opts: RetryOptions = {}): Promise<T> {
const {
maxAttempts = 5,
baseDelayMs = 500,
maxDelayMs = 30_000,
maxRetryAfterMs = 120_000,
deadlineMs = 180_000,
signal,
onRetry,
} = opts;
const giveUpAt = Date.now() + deadlineMs;
for (let attempt = 1; ; attempt++) {
try {
return await fn();
} catch (err) {
const decision = classify(err);
if (!decision.retry || attempt >= maxAttempts) throw err;
let delayMs: number;
if (decision.retryAfterMs !== undefined) {
// The server named a wait. Never retry sooner; give up if it is too long.
if (decision.retryAfterMs > maxRetryAfterMs) throw err;
delayMs = decision.retryAfterMs + Math.random() * 250;
} else {
// Full jitter: random point between 0 and the exponential ceiling.
const ceiling = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
delayMs = Math.random() * ceiling;
}
// Overall budget: if waiting would cross the deadline, surface the error now.
if (Date.now() + delayMs > giveUpAt) throw err;
onRetry?.({ attempt, delayMs, reason: decision.reason });
await sleep(delayMs, signal);
}
}
}
Using it with all three SDKs:
// usage.ts
import OpenAI from "openai";
import Anthropic from "@anthropic-ai/sdk";
import { GoogleGenAI } from "@google/genai";
import { withRetry } from "./retry";
// Turn SDK retries off so the two loops do not multiply attempts.
const openai = new OpenAI({ maxRetries: 0 });
const anthropic = new Anthropic({ maxRetries: 0 });
const gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
export async function demo(): Promise<void> {
const log = (i: { attempt: number; delayMs: number; reason: string }) =>
console.warn(`retry ${i.attempt} in ${Math.round(i.delayMs)}ms (${i.reason})`);
const a = await withRetry(
() => openai.responses.create({ model: "gpt-6-astra", input: "Say hello." }),
{ onRetry: log },
);
console.log(a.output_text);
const b = await withRetry(
() =>
anthropic.messages.create({
model: "claude-sonnet-5",
max_tokens: 256,
messages: [{ role: "user", content: "Say hello." }],
}),
{ onRetry: log },
);
console.log(b.content);
const c = await withRetry(
() => gemini.models.generateContent({ model: "gemini-3.8-flash", contents: "Say hello." }),
{ onRetry: log, maxAttempts: 4, deadlineMs: 60_000 },
);
console.log(c.text);
}
// Gemini alternative: let @google/genai retry generateContent itself.
export const geminiWithSdkRetries = new GoogleGenAI({
apiKey: process.env.GEMINI_API_KEY,
httpOptions: { retryOptions: { attempts: 5, httpStatusCodes: [429, 500, 503] } },
});
Both files were type-checked with tsc --noEmit (TypeScript 7.0.2, strict, moduleResolution: bundler) against openai@7.38.0, @anthropic-ai/sdk@0.135.0 and @google/genai@2.30.0. The classifier was also exercised offline with SDK-constructed errors (APIError.generate and new ApiError): an OpenAI 429 slow_down with retry-after: 2 retried after 2000 ms, credit_balance_exhausted and project_spend_limit_exceeded were not retried, an Anthropic spend-cap 429 was not retried, two simulated 529s recovered on the third call, a retry-after: 600 was thrown rather than waited, and a 2 second retry-after under a 1 second deadlineMs was thrown without sleeping. Plain Error subclasses carrying only status, code and headers (standing in for a duplicate SDK copy that fails instanceof) were classified the same way: a 529 retried and recovered, and the OpenAI quota and Anthropic spend-cap shapes were not retried. No live API calls were made. The OpenAI and Anthropic model IDs come from the examples on their error docs today; the Gemini ID is the current stable Flash model on the Gemini models page.
Why it is built this way#
Retry-After is a floor, not a target. When the server names a wait, the wrapper waits that long plus up to 250 ms of jitter, matching OpenAI's "treat it as a minimum and add a small random delay". It never retries sooner.
Long waits are handed back, not shortened. If the server asks for more than maxRetryAfterMs (two minutes by default), the wrapper throws the original error. Put the job on a queue with a scheduled retry time, rather than tying up a request handler, which is the "stop and defer" behaviour OpenAI recommends.
Attempts and time are both capped. maxAttempts alone is not enough: five attempts that each honour a two-minute Retry-After could block one call for about eight minutes. deadlineMs (three minutes by default) is checked before every wait, and if sleeping would cross it, the wrapper throws the last error instead. The check runs between attempts, so a request already in flight can still run past the deadline; set the SDK's own request timeout for that, and pass signal if you need to cancel a wait from outside. Gemini's generateContent docs list RPD and spend-based limits under the same RESOURCE_EXHAUSTED 429, which is exactly the case where an attempt cap on its own would wait far too long.
Full jitter when there is no header. Without a server delay, each wait is a random point between zero and min(maxDelayMs, baseDelayMs x 2^(attempt-1)). Randomising the whole interval stops a fleet of workers from retrying in lockstep, which is the "avoid synchronized retries" advice in the Bedrock 529 entry. The AWS SDKs' 2026 retry formula (opt-in for now) uses the same full-jitter shape.
Quota and billing errors throw on attempt one. OpenAI is detected by error.code (or a type of insufficient_quota). Anthropic's spend cap is detected through err.error.error.details.error_code, because the Anthropic SDK's APIError.error holds the full JSON body. Anthropic 402 and 400 errors fall through to the "other 4xx" branch and are not retried either.
Classification survives duplicate SDK copies. The instanceof checks give precise types, but a bundle that resolves an SDK once as CommonJS and once as ESM ends up with two APIError classes, and an error from one copy fails instanceof against the other. classifyByShape is the fallback: any error object with a numeric status is classified by status, code and the Anthropic details.error_code, so a 529 is still retried and a quota 429 is still refused.
Gemini is the weak spot, and the code says so. In @google/genai@2.30.0, ApiError exposes only status and message, with no headers or structured code. The wrapper cannot tell a daily quota_exceeded 429 from a per-minute one, so it retries every 429 and relies on maxAttempts and deadlineMs to stop. That is why the usage example caps Gemini at 4 attempts and 60 seconds. If daily quota exhaustion is a realistic failure for your app, alert on repeated Gemini 429s rather than letting them loop. A 504 DEADLINE_EXCEEDED is not retried, since Gemini's docs say to raise the client deadline instead.
Streaming needs its own path. Anthropic's errors page notes that streaming errors can arrive after a 200 response as SSE error events, outside the HTTP error mechanism. A wrapper around the call that opens the stream will not see an overload that arrives mid-stream, so handle the event in your stream consumer and decide there whether to restart.
Bedrock, Mistral, xAI, DeepSeek and Groq#
These do not need extra code beyond the same classify-then-wait pattern, but each has a detail worth knowing.
Amazon Bedrock. Let the AWS SDK's standard or adaptive retry mode handle ThrottlingException; that is what it is designed for. With the 2026 retry behavior enabled (opt-in through AWS_NEW_RETRIES_2026=true until it becomes the default in November 2026), the SDK uses a 1 second base for throttling versus 50 ms for transient errors; without the flag, your SDK's pre-2026 delays apply, and those use one base delay for every error type that varies by SDK. If throttling persists, the docs point to a quota increase, Reserved Tier or Provisioned Throughput rather than more retries. A 503 ServiceUnavailable (ServiceUnavailableException in the runtime API reference) or a 529 overloaded_error is a capacity signal, not your quota. For both, the docs suggest cross-Region inference where the model supports it; the 503 entry adds switching Region if the problem persists. Under the 2026 behavior the SDK reads x-amz-retry-after when a service sends one, but the retry behavior page does not mention the standard Retry-After that the 529 entry refers to, so read that header yourself before scheduling the next attempt.
Mistral. The glossary is the simplest of the lot: "For transient errors (429, 500, 502, 503, 504), implement exponential backoff", and the official Python and TypeScript SDKs include built-in retry logic. The error body's type field (rate_limit_error, server_error and so on) is a clean branch point.
xAI. The debugging page documents 429 on inference endpoints as reaching your rate limit, with the fix being a lower request rate or a higher limit in xAI Console.
DeepSeek. A 429 means you are sending too fast or exceeded a concurrency or QPS limit, so pace requests. 503 Server Overloaded and 500 are "retry after a brief wait". 402 Insufficient Balance means top up; do not retry.
Groq. 429 means throttle and respect your limits. The custom 498 only applies to the Flex Tier and means try again later. Groq also states that "You will not be charged for requests that return server error codes", so a 500, 502 or 503 retry costs time but not money.
If you call xAI, DeepSeek or Groq through the openai package with a custom baseURL, you also get the openai SDK's retry behaviour from the table above: two retries on 408, 409, 429 and 5xx with the 60 second Retry-After cap. That is a property of the SDK, not of those providers, so check that it matches their documented semantics (Groq's 498 is not in the SDK's retry set, for example).
Overload is not your limit: wait, fall back, or queue#
Retrying is the right first move, but repeated overloads say something different from repeated rate limits. A rate limit clears when your window resets. An overload clears when the provider recovers, and that can take longer than any reasonable request timeout.
%% title: Wait, fall back, or queue
flowchart TD
A[Retryable error] --> B{Which kind?}
B -->|529 / 503 repeats N times| C[Check provider status page]
C --> D[Fall back: other model or Region]
B -->|429 with long Retry-After| E[Queue and defer]
B -->|Quota or billing| F[Alert a human]
B -->|Short 429 or one-off 5xx| G[Backoff and retry]The docs back the fallback branch: Gemini's 503 guidance is to switch model or wait, and Bedrock suggests cross-Region inference for both its 503 and its 529, plus another Region when a 503 persists. Our piece on designing workflows around Claude outages covers the workflow side of this, including why 529 is not your usage limit. If you run several providers, moving retries and fallbacks into a gateway keeps the policy in one place; the gateway decision guide under Continue Reading weighs self-hosted against managed options.
Checklist#
- Turn SDK retries off (
maxRetries: 0) if you own the retry loop, or skip your loop and tune the SDK. Do not run both. - Branch on the error code, not only the status. OpenAI, Anthropic, Gemini and DeepSeek all document a 429 or 402 that must not be retried.
- Treat
Retry-Afteras a minimum, and give up (defer to a queue) when it is longer than you can wait. - Always add jitter. Full jitter when there is no server delay, a small random addition when there is.
- Cap both total attempts and total elapsed time (
maxAttemptsanddeadlineMsin the wrapper above), and set a per-request timeout on the SDK client. - Log the request ID on every failure: OpenAI's SDK reads it from
x-request-id, Anthropic's fromrequest-id, and both expose it aserr.requestID. - Handle streaming errors that arrive after a 200 in your stream consumer.
- Alert on quota and billing errors. They need a person with a credit card or an admin console, not a backoff schedule.
FAQ#
Should I retry an OpenAI 429 insufficient_quota error?#
No. OpenAI's error-codes guide says retrying billing, spend or quota errors will not restore API access. Inspect error.code to see the cause (credit_balance_exhausted, organization_spend_limit_exceeded, project_spend_limit_exceeded or organization_usage_limit_exceeded), because error.type can still read insufficient_quota for all of them. Fix the credits or limits first.
What is Anthropic error 529 overloaded_error, and is it my rate limit?#
No. A 529 means the API is temporarily overloaded across users. Retry it with exponential backoff and jitter. Your own rate limit is a 429 rate_limit_error, which may carry a retry-after header (Anthropic notes that not every 429 does). The exception is the tier spend cap: a 429 with no retry-after and error.details.error_code of enforced_spend_limit_reached. It does not clear until 00:00 UTC on the first of next month unless you move to a higher tier or request a higher limit, so do not retry it. A 429 on Claude Code workspace traffic can also mean that workspace's own spend limit; that one does carry retry-after, so honour it or defer the work.
Does the OpenAI or Anthropic SDK honour Retry-After?#
Both read retry-after-ms and retry-after and retry twice by default. In openai@7.38.0, a delay over 60 seconds is not used and the SDK's 0.5s to 8s backoff applies instead. @anthropic-ai/sdk@0.135.0 honours any positive delay. Neither checks the error code first, so a quota 429 still uses up its retries.
Does @google/genai retry 429 RESOURCE_EXHAUSTED automatically?#
It depends on the method. In @google/genai@2.30.0, models.generateContent does not retry unless you set httpOptions.retryOptions; when you do, the defaults are 5 attempts with 1s to 60s backoff on 408, 429, 500, 502, 503 and 504, and Retry-After is not read. interactions.create retries up to 4 times by default on 408, 409, 429 and 5XX, with waits capped at 8 seconds.
What is a good backoff schedule for AI APIs?#
Start around 0.5 to 1 second, double each attempt, cap the computed delay somewhere between 8 and 60 seconds, apply full jitter, and stop after 3 to 5 attempts or an overall deadline, whichever comes first. Always wait at least as long as Retry-After when it is present. The SDK defaults are reasonable reference points: OpenAI and Anthropic use 0.5s to 8s with 2 retries, Gemini's opt-in retries use 1s to 60s with 5 attempts, and the AWS SDKs, with the 2026 retry behavior enabled (opt-in until November 2026), use 3 attempts with a 20 second backoff cap (25 seconds when a service sends x-amz-retry-after). Without that flag, AWS SDK defaults vary by SDK.
Sources#
- OpenAI error codes: https://developers.openai.com/api/docs/guides/error-codes
- OpenAI rate limits: https://developers.openai.com/api/docs/guides/rate-limits
- Anthropic (Claude API) errors: https://platform.claude.com/docs/en/api/errors
- Anthropic (Claude API) rate limits: https://platform.claude.com/docs/en/api/rate-limits
- Gemini API errors (Interactions API): https://ai.google.dev/gemini-api/docs/api-errors
- Gemini generateContent errors: https://ai.google.dev/gemini-api/docs/generate-content/api-errors
- Amazon Bedrock API error codes: https://docs.aws.amazon.com/bedrock/latest/userguide/troubleshooting-api-error-codes.html
- AWS SDKs retry behavior (opt-in 2026 behavior): https://docs.aws.amazon.com/sdkref/latest/guide/feature-retry-behavior.html
- AWS announcement of the updated SDK retry behavior: https://aws.amazon.com/blogs/developer/announcing-updated-retry-behavior-for-aws-sdks-and-tools/
- Gemini models: https://ai.google.dev/gemini-api/docs/models
- Mistral error glossary: https://docs.mistral.ai/resources/error-glossary
- xAI debugging: https://docs.x.ai/developers/debugging
- DeepSeek error codes: https://api-docs.deepseek.com/quick_start/error_codes
- Groq errors: https://console.groq.com/docs/errors
- Groq rate limits: https://console.groq.com/docs/rate-limits
- openai-node (
src/client.ts,src/core/error.tsin 7.38.0): https://github.com/openai/openai-node - anthropic-sdk-typescript (
src/client.ts,src/core/error.tsin 0.135.0): https://github.com/anthropics/anthropic-sdk-typescript - js-genai (
HttpRetryOptions, interactions retry config in 2.30.0): https://github.com/googleapis/js-genai
Continue Reading#
- Claude Fable 5 API production patterns and rate limits - Claude-specific rate limit strategy, streaming latency and fallback chains.
- Claude Code retry watchdog for unattended runs - how Claude Code waits out 429 and 529 errors in CI and overnight jobs.
- Self-hosted vs managed AI gateway - when to move retries and fallbacks out of app code and into a gateway.
Get the next deep dive like this in your inbox
One email a week on AI Development and the rest of the AI dev stack. Free.
Read next
Claude Fable 5 API: Production Integration Patterns, Rate Limits, and Migration Gotchas
Claude Fable 5 API production guide - adaptive thinking, rate limit strategy, streaming latency, fallback chains and a migration checklist from Opus 4.8.
9 min readClaude API Reliability: Error Handling Best Practices
Claude API error handling for production: retry shapes, backoff with jitter, circuit breakers, fallback chains, and the observability to debug failures at 3am.
10 min readClaude Outages Are a Workflow Design Problem
Claude outages and 529 overloads test your AI coding workflow. Design for them with small task slices, receipts, model-switch paths and local artifacts.
7 min readNew here? Start with
Technical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.





