Claude API Retry & Exponential Backoff Pattern Guide
When the Claude API returns a 429 rate limit error or a transient 500/529 overload error, the correct response is not to fail immediately or to hammer the endpoint again right away. The standard pattern is exponential backoff with jitter: wait a short, randomized amount of time before retrying, and double that wait on each subsequent failure, up to a capped maximum.
This article walks through exactly how to implement that pattern for Claude API calls, which status codes warrant a retry, which don't, and how to avoid common mistakes like retrying non-idempotent requests or retrying forever on a bad API key.
Why you need retry logic at all
The Claude API, like any hosted LLM endpoint, has rate limits (requests per minute, tokens per minute) and occasional capacity constraints during high load. Even a well-architected client will eventually see:
429 Too Many Requests— you've exceeded your rate limit529 Overloaded— Anthropic's servers are at capacity500 Internal Server Error— a transient server-side failure- Network-level errors — timeouts, connection resets
None of these mean your request was malformed. They mean "try again, but not immediately." Retrying instantly makes the problem worse — you add load to an already-stressed system and often get rate-limited harder. Exponential backoff solves this by spacing out retries so the system has time to recover.
The core algorithm
The pattern has three parts:
- Base delay — the initial wait time (commonly 500ms–1s)
- Exponential growth — multiply the delay by a factor (usually 2x) on each retry
- Jitter — add randomness so multiple clients don't retry in synchronized waves
A typical formula:
delay = min(maxDelay, baseDelay * (2 ^ attempt)) + random(0, jitterMs)
Capping the delay (maxDelay) matters — without it, a client that's retried 8 times could end up waiting minutes, which is usually worse than just failing fast and alerting.
Implementation in JavaScript
Here's a reusable retry wrapper for any fetch-based Claude API call:
async function withBackoff(fn, {
maxRetries = 5,
baseDelayMs = 500,
maxDelayMs = 20000,
} = {}) {
let attempt = 0;
while (true) {
try {
const response = await fn();
if (response.status === 429 || response.status === 529 || response.status >= 500) {
throw { retryable: true, status: response.status };
}
return response;
} catch (err) {
attempt++;
const retryable = err.retryable || err.name === 'TypeError'; // network error
if (!retryable || attempt > maxRetries) {
throw err;
}
const expDelay = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
const jitter = Math.random() * expDelay * 0.3;
const delay = expDelay + jitter;
console.warn(`Retry ${attempt}/${maxRetries} after ${Math.round(delay)}ms (status ${err.status})`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
Usage against a Claude-compatible endpoint:
const result = await withBackoff(() =>
fetch('https://api.subtoapi.app/v1/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SUBTOAPI_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'claude-sonnet-4-5',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Summarize this ticket.' }],
}),
})
);
Which status codes should trigger a retry
Not every error is retryable. Retrying a 400 Bad Request just gets you the same error five times in a row with extra latency.
Retry these:
429— rate limit exceeded500— transient server error529— server overloaded- Network timeouts / connection resets
Do not retry these:
400— malformed request (fix the payload, don't retry)401— invalid or missing API key403— permission denied404— invalid endpoint or model name
A common mistake is wrapping the entire request lifecycle in a generic "retry on any exception" block. That silently retries on bugs in your own request-building code, which just burns tokens and delays the real error from surfacing.
Respect the Retry-After header
When present, the 429 and 529 responses often include a Retry-After header indicating how long to wait before the next attempt. When it's available, prefer it over your own computed exponential delay — it reflects the server's actual state:
const retryAfter = response.headers.get('retry-after');
const delay = retryAfter ? Number(retryAfter) * 1000 : computedExpBackoff;
Backoff for streaming requests
Backoff is straightforward for non-streaming requests because you retry the whole call. For streaming, you need to decide: if the stream drops mid-response, do you retry from scratch or resume? Claude's streaming API does not support resuming a partial stream, so the safe approach is to retry the entire request and discard any partial output you received. Track whether you've already appended partial content to a UI buffer, and clear it before retrying. See /docs/streaming for details on the event format you'll be parsing when you rebuild the retry logic around SSE.
Idempotency and side effects
If your Claude API calls trigger side effects via tool use — writing to a database, calling an external API, sending an email — exponential backoff retries can cause duplicate side effects if the first "failed" request actually succeeded server-side but the response was lost in transit. Design tool handlers to be idempotent (use request IDs, upserts instead of inserts) before you wire up aggressive retry logic. See /docs/tools for how tool calls are structured.
Where SubToAPI fits
If you're running Claude access through SubToAPI, the retry pattern above works unchanged — it's a standard HTTPS API using sub_live_... application keys, so any backoff wrapper built for the native Claude API applies directly. The dashboard also surfaces per-key usage and error rates, which is useful for tuning your maxRetries and baseDelayMs based on real observed rate-limit frequency rather than guessing. Check /docs/quickstart to get a key, or /pricing if you're evaluating plans for a team.
Questions
How many retries should I configure for Claude API calls? 3–5 is typical for interactive user-facing requests. Background jobs can afford more (8–10) since latency matters less there.
Should I use the same backoff strategy for rate limits and server errors? You can use the same exponential curve, but prefer the Retry-After header when present for 429/529 — it's more accurate than a guessed delay.
Does exponential backoff work the same way for streaming requests? Not quite — streams can't resume mid-way, so a dropped stream means retrying the full request and discarding any partial output already rendered.