Skip to content

LLM API Timeouts, Retries and Jitter, Done Right

Core Concept LearningOctober 22, 202616 min read

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.

The retry decision: classify the error, honour retry-after if the server sent one, otherwise wait a random full-jitter delay, and give up when attempts or the time budget run out
The retry decision: classify the error, honour retry-after if the server sent one, otherwise wait a random full-jitter delay, and give up when attempts or the time budget run out

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_down 429s 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 no retry-after header and error.details.error_code set to enforced_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_exceeded and organization_usage_limit_exceeded mean 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.

Which model API errors to retry, which never to retry, and which need a decision
Which model API errors to retry, which never to retry, and which need a decision

Quick reference

  • Match on the SDK's typed exceptions and the structured type and code fields, never on the error message text.
  • Log the request-id header (Claude) on every failure; it is what support asks for.
  • OpenAI's Python SDK raises RateLimitError for 429 and InternalServerError for 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 with max_retries on the client or per request.
  • Retried conditions: connection errors and timeouts, plus 408, 409, 429 and all 5xx responses. A non-standard x-should-retry response 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 read retry-after-ms first, then retry-after as seconds or an HTTP date, and wait exactly that long when it is present. The OpenAI SDK does not retry at all when retry-after exceeds 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/2 plus a random time between 0 and v/2.
  • Decorrelated jitter: wait a random time between base and 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.

Simulated retry storm: 1,000 clients that fail at the same instant and retry four times; the busiest 100 ms window gets 1,000 requests with no jitter and 158 with full jitter
Simulated retry storm: 1,000 clients that fail at the same instant and retry four times; the busiest 100 ms window gets 1,000 requests with no jitter and 158 with full jitter

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-after is 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-after applies to all of them; a shared limiter or queue handles this better than per-worker sleeps.
  • Parse retry-after as 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.

Scenarios run against a local stub of the Messages API (BASE shortened to 0.05 s for the test)
ScenarioCallsOutcome
529 overloaded twice, then success3Returned the response
429 with retry-after: 1, then success2Waited 1.01 s, returned the response
429 spend cap (enforced_spend_limit_reached)1Raised RateLimitError without retrying
400 bad request1Raised BadRequestError without retrying
529 on every call5Raised OverloadedError after the last attempt
429 with retry-after: 500 (deadline 120 s)1Raised immediately instead of sleeping
Stream: overloaded error before any text2Retried; caller saw "Hello" once
Stream: overloaded error after "Hel"1Raised APIStatusError with status 200; no repeated text
Stream slower than a 1 s deadline1Raised TimeoutError after three chunks

Quick reference

  • The same structure works for the OpenAI SDK: its exceptions have the same status_code, response and body attributes, and its error codes go in NEVER_RETRY.
  • Tune MAX_ATTEMPTS and DEADLINE per 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.

Three timeouts for a model call: a short connect timeout, an idle timeout between chunks that pings keep alive during streaming, and an overall deadline that bounds retries and total time
Three timeouts for a model call: a short connect timeout, an idle timeout between chunks that pings keep alive during streaming, and an overall deadline that bounds retries and total time

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.

Share:

References

  1. Claude docs: API errors — checked Oct 22, 2026
  2. Claude docs: Rate limits (retry-after and spend caps) — checked Oct 22, 2026
  3. Claude docs: Streaming messages (error and ping events) — checked Oct 22, 2026
  4. OpenAI docs: Error codes — checked Oct 22, 2026
  5. Marc Brooker, Exponential Backoff And Jitter, AWS Architecture Blog — checked Oct 22, 2026
  6. aws-samples/aws-arch-backoff-simulator (jitter formulas) — checked Oct 22, 2026
  7. anthropic-sdk-python (retry and timeout implementation) — checked Oct 22, 2026
  8. HTTPX docs: Timeouts — checked Oct 22, 2026
SH

Shoaib Hossain

Lead Author & Systems Architect

Software engineer and distributed systems architect specializing in backend scalability, cloud-native infrastructure, databases, and AI engineering workflows. Author and maintainer of Core Concept Learning.

Human-Engineered & Fact-CheckedOriginal Visual DiagramsEditorial Standards →Send Feedback

Related Articles

Asking a model to "reply in JSON" works until it does not: a missing field, a trailing comment, a number returned as a s…

Read

With the release of OpenAI's reasoning model series (such as o3-mini), developers gain direct control over inference-tim…

Read

An agent that runs for four seconds inside one HTTP request can afford to fail: the user retries and nothing else happen…

Read

Keep learning

Follow a structured path or browse all courses to go deeper.