LLM API Timeouts, Retries and Jitter, Done Right
Model APIs fail in ordinary ways: rate limits, overloaded capacity, server errors, dropped connections and calls that run longer than a timeout. Most of these are transient, so retrying is correct. But a retry policy that treats every error the same will hammer an API that is already overloaded, retry errors that can never succeed, and repeat text the user has already seen when a stream fails halfway through.
This guide builds a retry policy for model APIs from the providers' own documentation and the SDKs' source code. It covers which errors to retry (including the 429s that look retryable but are not), what the official Python SDKs already do by default, why backoff needs jitter, how to honour retry-after, and how to set timeouts for long and streaming calls. The policy is a 74-line module for the Claude Python SDK. It was tested against a local stub server that returns scripted errors, and the results are in this guide.
The facts are from the Claude API errors, rate-limit and streaming documentation, OpenAI's error-code guide, the source of anthropic 1.15 and openai 3.35, and Marc Brooker's Exponential Backoff and Jitter on the AWS Architecture Blog. For queueing work when you hit rate limits, see AI API Rate Limits and Queues.
Why model calls need a deliberate retry policy
Three properties make model APIs different from a typical internal service.
Failures are often about capacity, not bugs. Claude returns 529 overloaded_error when the API is under high load across all users, and 429 rate_limit_error when your organization exceeds its limits. OpenAI returns 503 server_is_overloaded when a model lacks capacity, and 429 with a slow_down code when your request rate rises too fast. All of these are worth retrying, but only after the pressure has eased.
Calls are slow and expensive. A single request can run for minutes, and a retried request is billed again. A policy that retries too eagerly turns one slow failure into several slow failures.
Responses stream. Once tokens have reached the user, the request cannot simply be retried: the new response would start from the beginning and likely differ.
A good policy answers three questions for every failure: should this be retried at all, how long to wait first, and is there still time and budget to try again. The rest of this guide answers each one.
Quick reference
- Claude's docs note that a sharp increase in your own usage can trigger 429s from acceleration limits, and recommend ramping traffic gradually.
- OpenAI documents that
slow_down429s can occur even within your requests-per-minute and tokens-per-minute limits. - Retries handle short failures. For longer outages, fail over to another model or provider; see AI Provider Failover Patterns.
Remember this
Retry capacity and network failures, but wait first, stop in time, and never repeat streamed output.
Which errors to retry
Start from the status code, then check the error body, because the status alone is ambiguous.
Retry: connection errors and timeouts, 408, 429 rate limits, 500, 502, 503, 504, and Claude's 529. Claude's docs say to retry 500 errors with exponential backoff. OpenAI's say to retry 500s after a brief wait, and to follow Retry-After on 429 and 503.
Never retry: 400 (bad request), 401 (authentication), 403 (permission), 404, and 413 (request too large). The same request will fail the same way; fix it instead.
The 429s that never succeed. Both providers also use 429 for limits that a retry cannot clear:
- On Claude, reaching your tier's monthly spend cap returns 429
rate_limit_error, the same type as a rate limit, but with noretry-afterheader anderror.details.error_codeset toenforced_spend_limit_reached. The docs state that retrying, including the SDK's automatic retries, fails until access resumes. - On OpenAI, 429s with the codes
credit_balance_exhausted,organization_spend_limit_exceeded,project_spend_limit_exceededandorganization_usage_limit_exceededmean billing or quota limits. OpenAI's guide says retrying them won't restore access.
Needs a decision: a 409 conflict (Claude's docs say to resolve the conflict, then retry), and any error after a stream has started sending text, covered later.
The boundary is that error bodies change. Claude's docs say the type values may grow over time, and OpenAI moved video overload from 429 to 503. Treat unknown types as non-retryable, and log them so you notice new ones.
Quick reference
- Match on the SDK's typed exceptions and the structured
typeandcodefields, never on the error message text. - Log the
request-idheader (Claude) on every failure; it is what support asks for. - OpenAI's Python SDK raises
RateLimitErrorfor 429 andInternalServerErrorfor 503, so catch both when handling throttling.
Remember this
Retry transient capacity and network errors; never retry client errors or spend and quota 429s.
What the official SDKs already do
Before writing anything, know what you get for free. The anthropic (1.15) and openai (3.35) Python SDKs share the same retry design:
- Two retries by default (
DEFAULT_MAX_RETRIES = 2), configurable withmax_retrieson the client or per request. - Retried conditions: connection errors and timeouts, plus 408, 409, 429 and all 5xx responses. A non-standard
x-should-retryresponse header overrides the decision when the server sends it. - Backoff: 0.5 seconds doubled on each retry, capped at 8 seconds, multiplied by a random factor between 0.75 and 1.0.
retry-after: both readretry-after-msfirst, thenretry-afteras seconds or an HTTP date, and wait exactly that long when it is present. The OpenAI SDK does not retry at all whenretry-afterexceeds 120 seconds.- Timeouts: 600 seconds by default, with 5 seconds to connect.
For many applications, the defaults are enough. They retry the right errors, respect the server's hints and stop after three attempts. Change them when you need something they do not do: a total deadline across retries, refusing to retry spend-cap 429s, different handling for streams, or more attempts for a background job.
The jitter is the weakest part for large fleets. A 0–25% reduction spreads retries over a small window, so many clients failing together still retry close together; the next section measures how much that matters.
Quick reference
- Disable SDK retries (
max_retries=0) when you add your own policy, or the two multiply: three SDK attempts inside five of yours is up to fifteen requests. - The SDKs also retry 409, which Claude's docs say needs resolving first; for the Messages API it is rare.
- Check these values again after upgrading the SDK; they are constants in the client code, not documented guarantees.
Remember this
The SDKs retry the right errors twice with short, lightly jittered backoff; replace them only for deadlines, streams or larger fleets.
Exponential backoff needs jitter
Exponential backoff waits longer after each failure: base × 2^attempt, capped at a maximum. It reduces how often each client retries, but if many clients failed at the same moment, they all retry at the same moments too. Marc Brooker's AWS article shows the result: backoff without jitter still produces clusters of calls with gaps between them, so contention barely falls.
The fix is randomness. The article compares three schemes, using v = min(cap, base × 2^attempt):
- Full jitter: wait a uniform random time between 0 and
v. - Equal jitter: wait
v/2plus a random time between 0 andv/2. - Decorrelated jitter: wait a random time between
baseand three times the previous wait, capped.
In its simulation, full and equal jitter did about the same amount of work and decorrelated did more. Equal jitter took much longer to finish, and the author calls equal jitter the loser and no-jitter backoff the clear loser.
To see the effect for a model API, this guide simulated 1,000 clients that fail at the same instant and retry four times, with base 1 second and cap 30, counting requests in the busiest 100 ms window. With no jitter, all 1,000 retries land in the same window. The SDK-style 0–25% jitter (with its 0.5-second base) still puts 791 there. Equal jitter brings it to 212, and full jitter to 158. Full jitter also finished sooner than no jitter: the mean time of the fourth retry was 7.6 seconds against 15.0.
This is the worst case, since real clients rarely fail at exactly the same instant. But it is the case that matters, because a provider outage or a deploy makes many clients fail together.
Quick reference
- Full jitter can produce a near-zero wait. That is fine for the first retry; a cap on attempts bounds the total.
- Brooker notes that jitter does not change the underlying N² work of heavy contention; it reduces it at realistic contention levels.
- The simulation code is short: generate each client's four waits, add them up, and count arrival times per 100 ms bucket.
Remember this
Backoff without jitter keeps retries synchronized; full jitter spreads them out and often finishes sooner.
Honour retry-after, and know when to stop
When the server tells you how long to wait, believe it. Claude's rate-limit docs describe retry-after as the number of seconds to wait before retrying and say that earlier retries will fail. OpenAI's guide says to wait at least as long as Retry-After specifies and to use exponential backoff with jitter only when the header is missing. A random backoff shorter than the server's hint just spends an attempt on a request that will be rejected.
Two cases break the simple rule:
- No header. Not every 429 carries
retry-after. Claude's spend-cap 429 never does, so a missing header on a 429 is a reason to check the error code before backing off. - A wait longer than you can afford. If the server says 300 seconds and a user is waiting, sleeping is the wrong answer. Fail fast with a clear error, move the work to a queue, or fail over. The OpenAI SDK has this rule built in: it refuses to retry when
retry-afteris over 120 seconds.
The policy below applies the second rule as a deadline: before any sleep, it checks whether the elapsed time plus the wait would exceed the caller's budget, and raises immediately if so. That turns "wait 500 seconds" into a fast, explicit failure the caller can handle.
Quick reference
- Claude also returns
anthropic-ratelimit-*headers on responses, which show remaining capacity before you hit a 429; use them to slow down early. - When several workers share a rate limit, one worker's
retry-afterapplies to all of them; a shared limiter or queue handles this better than per-worker sleeps. - Parse
retry-afteras seconds first and as an HTTP date second; the SDKs accept both.
Remember this
Wait at least as long as retry-after says, but give up immediately when that wait exceeds your deadline.
The retry policy in code
Here is the complete policy for the Claude Python SDK. It turns off the SDK's own retries, classifies errors by status and type, honours retry-after, uses full jitter, enforces a deadline, and treats streams separately:
1import random, time2import anthropic3 4# Turn off the SDK's own retries so this policy is the only one.5client = anthropic.Anthropic(max_retries=0, timeout=anthropic.Timeout(120.0, connect=5.0))6 7RETRY_STATUS = {408, 429, 500, 502, 503, 504, 529}8RETRY_TYPES = {"overloaded_error", "api_error", "rate_limit_error", "timeout_error"}9NEVER_RETRY = {"enforced_spend_limit_reached"}10BASE, CAP, MAX_ATTEMPTS, DEADLINE = 1.0, 30.0, 5, 120.011 12def error_info(err):13 body = err.body if isinstance(err.body, dict) else {}14 error = body.get("error") or {}15 return error.get("type"), (error.get("details") or {}).get("error_code")16 17def retryable(err):18 if isinstance(err, anthropic.APIConnectionError): # includes APITimeoutError19 return True20 if not isinstance(err, anthropic.APIStatusError):21 return False22 kind, code = error_info(err)23 if code in NEVER_RETRY:24 return False25 # Errors inside a stream arrive with the stream's HTTP 200, so check the type too.26 return err.status_code in RETRY_STATUS or kind in RETRY_TYPES27 28def server_wait(err):29 headers = err.response.headers if isinstance(err, anthropic.APIStatusError) else {}30 for name, scale in (("retry-after-ms", 0.001), ("retry-after", 1.0)):31 try:32 return float(headers[name]) * scale33 except (KeyError, TypeError, ValueError):34 continue35 return None36 37def backoff(attempt, err, started):38 wait = server_wait(err)39 if wait is None:40 wait = random.uniform(0, min(CAP, BASE * 2 ** attempt)) # full jitter41 if time.monotonic() - started + wait > DEADLINE:42 raise err # waiting would blow the caller's time budget43 time.sleep(wait)44 45def remaining(started):46 return anthropic.Timeout(max(1.0, DEADLINE - (time.monotonic() - started)), connect=5.0)47 48def create(**params):49 started = time.monotonic()50 for attempt in range(MAX_ATTEMPTS):51 try:52 return client.messages.create(**params, timeout=remaining(started))53 except anthropic.APIError as err:54 if not retryable(err) or attempt == MAX_ATTEMPTS - 1:55 raise56 backoff(attempt, err, started)57 58def stream_text(**params):59 started = time.monotonic()60 for attempt in range(MAX_ATTEMPTS):61 sent_any = False62 try:63 with client.messages.stream(**params, timeout=remaining(started)) as stream:64 for text in stream.text_stream:65 if time.monotonic() - started > DEADLINE:66 raise TimeoutError("stream ran past the deadline")67 sent_any = True68 yield text69 return70 except anthropic.APIError as err:71 # Once text has reached the caller, a retry would repeat it: surface the error.72 if sent_any or not retryable(err) or attempt == MAX_ATTEMPTS - 1:73 raise74 backoff(attempt, err, started)Call create(...) and stream_text(...) with the same arguments as client.messages.create. remaining() passes what is left of the deadline as the per-request timeout, so a single slow call cannot use the whole budget and still be retried afterwards.
The table shows each scenario the stub server was scripted to produce. One result is worth noticing. The error raised inside the stream had status 200. The SDK builds the exception from the stream's original HTTP response, which succeeded before the error event arrived. A policy that checks only status_code would treat a mid-stream overloaded_error as non-retryable, which is why retryable also checks the error type.
| Scenario | Calls | Outcome |
|---|---|---|
| 529 overloaded twice, then success | 3 | Returned the response |
429 with retry-after: 1, then success | 2 | Waited 1.01 s, returned the response |
429 spend cap (enforced_spend_limit_reached) | 1 | Raised RateLimitError without retrying |
| 400 bad request | 1 | Raised BadRequestError without retrying |
| 529 on every call | 5 | Raised OverloadedError after the last attempt |
429 with retry-after: 500 (deadline 120 s) | 1 | Raised immediately instead of sleeping |
| Stream: overloaded error before any text | 2 | Retried; caller saw "Hello" once |
| Stream: overloaded error after "Hel" | 1 | Raised APIStatusError with status 200; no repeated text |
| Stream slower than a 1 s deadline | 1 | Raised TimeoutError after three chunks |
Quick reference
- The same structure works for the OpenAI SDK: its exceptions have the same
status_code,responseandbodyattributes, and its error codes go inNEVER_RETRY. - Tune
MAX_ATTEMPTSandDEADLINEper call site: an interactive request might allow 20 seconds, a nightly batch job much longer. - Log each retry with the attempt number, wait, status, error type and request ID, so you can see retry storms in your metrics.
Remember this
Classify by status and error type, prefer retry-after, use full jitter, and enforce one deadline across all attempts.
Timeouts for long and streaming calls
A single timeout value is not enough, because the SDKs' timeouts are not total durations. Both SDKs use an httpx-style Timeout(600, connect=5). The HTTP client's documentation defines the read timeout as the maximum wait for a chunk of data, not for the whole response. A non-streaming call sends nothing until it finishes, so there the read timeout behaves like a total limit. A stream sends chunks continuously, and the API also sends ping events, so a stream can run far past the read timeout without tripping it.
So set three limits:
- Connect: a few seconds. If the host is unreachable you want to know immediately.
- Idle (read): the longest acceptable gap between chunks. For a stream, a gap longer than this means the connection is stuck.
- Deadline: the total time for the call and all its retries, enforced in your own code. The policy above checks it before every sleep and on every streamed chunk.
Long non-streaming calls have one more problem: some networks drop idle connections, and the client never receives an answer. Claude's docs recommend streaming or the Message Batches API for long requests, especially those over 10 minutes. The SDK enforces this: it raises an error if a non-streaming request's max_tokens implies more than 10 minutes. It estimates 60 minutes per 128,000 tokens, which works out to about 21,333 tokens. It also sets TCP keep-alive on its connections.
Quick reference
- Use streaming for any call that might be long, even if you only need the final message; the SDK can collect the stream into a complete
Message. - For work no user is waiting for, the Batch API avoids long-lived connections entirely: you poll for results.
- Do not set the deadline shorter than a normal successful call takes; measure your p99 latency first.
Remember this
Use a short connect timeout, an idle timeout between chunks, and your own deadline for the total; stream anything long.
Retrying streams without repeating output
Claude's streaming docs warn that errors can arrive inside the event stream after the API has already returned 200. For example, an overloaded_error arrives as an error event, where a non-streaming call would have received HTTP 529. Whether to retry depends on what the caller has already seen:
- Before any text was delivered: retry exactly as for a non-streaming call. The caller never knows.
- After text was delivered: do not retry silently. A new request starts the response again from the beginning, so the user would see the opening twice, and sampling means the second version can differ from the first. Raise the error and let the caller decide: show a "regenerate" button, or retry and replace what was shown.
stream_text implements this with one flag, sent_any, set when the first chunk is yielded. The stub test shows both paths: an error before the first chunk was retried and the caller saw "Hello" once; an error after "Hel" was raised, and the caller saw "Hel" and an exception rather than "HelHello".
Tool use adds a sharper version of the same problem. If the stream contained a tool call and your code already ran it, retrying the request may run it again. Make tools with side effects idempotent, or record which tool calls already ran. For handling cancellation and partial tool calls in streams, see Streaming Tool Calls and Cancellation.
Quick reference
- Buffer the first few hundred milliseconds of a stream if you want more room to retry invisibly; it trades a little latency for fewer visible errors.
- Handle unknown event types gracefully; Claude's docs say new event types may be added.
- If you collect the stream server-side and only return the final message, you can retry the whole call safely, as with non-streaming requests.
Remember this
Retry a stream only before its first output; after that, surface the error instead of repeating text.
Retry budgets, layers, and when retries make things worse
Retries add load at the worst moment: when a service is failing, every client sends more requests. Three practices keep that bounded.
Retry in one layer only. If a gateway retries three times, your service retries three times and the SDK retries three times, one user request can reach the API 27 times. Pick one layer, usually the one closest to the API that knows the deadline, and set max_retries=0 everywhere else.
Cap retries across the fleet. A per-request limit does not stop a fleet of workers from multiplying traffic during an outage. A retry budget allows retries only while they stay below a fraction of total requests, and a circuit breaker stops calling a failing dependency for a while. See Circuit Breaker Pattern.
Know what a retry repeats. A model call is safe to retry in the sense that it changes nothing, but it is billed again and may give a different answer. A tool call with side effects is not safe to retry unless it is idempotent. In an agent, retry the model request and guard the tools; see Build an Agent Loop From Scratch.
When retries are exhausted, fail clearly: return an error that says the model was unavailable, keep the request ID, and offer a way to try again later.
Quick reference
- Alert on the retry rate, not only on final failures; rising retries are the early warning of an outage.
- Separate limits for interactive and background traffic keep batch jobs from using the retry headroom users need.
- Failover to another model changes output quality and cost, so test it before an outage, not during one.
Remember this
Retry in one layer, bound retries across the fleet, and never retry side effects that are not idempotent.
Practice: test your policy against a stub
Build a local stub of the Messages API and break your policy on purpose:
1. Write an HTTP server that answers POST /v1/messages from a script of responses, and point the SDK at it with ANTHROPIC_BASE_URL.
2. Script the nine scenarios in the table above and record how many calls each one makes.
3. Run them with the SDK's default retries, then with llm_retry.py, and compare the call counts for the spend-cap and retry-after: 500 cases.
4. Make the stub send an error event after the first text delta, and print err.status_code in your handler.
5. Simulate 1,000 clients retrying four times with no jitter, SDK-style jitter and full jitter, and count requests in the busiest 100 ms window.
6. Add a gateway retry around your own policy and count the API calls for an always-529 scenario.
Pass criterion: spend-cap and 400 errors make exactly one call, a long retry-after fails without sleeping, a mid-stream error after text never repeats output, and your simulation shows full jitter with a lower peak than no jitter.
Quick reference
- Keep the stub in your test suite; it catches regressions when you upgrade the SDK.
- Set the backoff base very small in tests so the suite runs in seconds.
- Record real error rates from production logs before choosing attempt counts.
Remember this
A scripted stub server is the cheapest way to prove what your retry policy does under each failure.
Key takeaway
A retry policy for model APIs answers three questions for every failure. Should it be retried? Yes for connection errors, timeouts, 408, 429 rate limits, 5xx and Claude's 529. No for client errors, and no for the spend, credit and quota 429s that both providers document as never succeeding on retry. How long to wait? As long as retry-after says, or a full-jitter random delay when it is absent, because backoff without jitter keeps retrying clients synchronized. Is there budget left? Check attempts and a total deadline before every sleep, and fail fast when the server asks for a wait you cannot afford.
Timeouts need the same care: a short connect timeout, an idle timeout between chunks, and a deadline you enforce yourself, with streaming for anything long. Streams can be retried only before their first output, and an error inside a stream arrives with status 200. The official SDKs get much of this right by default; replace them when you need deadlines, stream handling or fleet-scale jitter. For what to do when retries are not enough, see AI Provider Failover Patterns; for pacing requests under rate limits, see AI API Rate Limits and Queues.
Practice (60 min): build the stub server and run the six steps above.
References
- Claude docs: API errors — checked Oct 22, 2026
- Claude docs: Rate limits (retry-after and spend caps) — checked Oct 22, 2026
- Claude docs: Streaming messages (error and ping events) — checked Oct 22, 2026
- OpenAI docs: Error codes — checked Oct 22, 2026
- Marc Brooker, Exponential Backoff And Jitter, AWS Architecture Blog — checked Oct 22, 2026
- aws-samples/aws-arch-backoff-simulator (jitter formulas) — checked Oct 22, 2026
- anthropic-sdk-python (retry and timeout implementation) — checked Oct 22, 2026
- HTTPX docs: Timeouts — checked Oct 22, 2026
Shoaib Hossain
Lead Author & Systems ArchitectSoftware engineer and distributed systems architect specializing in backend scalability, cloud-native infrastructure, databases, and AI engineering workflows. Author and maintainer of Core Concept Learning.
Related Articles
Explore this topic