Retries and rate-limit backoff
Written by a person. Last read by a person on 2026-09-04, 4 days ago. Its facts were checked by the eval suite on 2026-09-07.
Your calls fail intermittently and you are wondering whether to retry, you are seeing 429s that never clear no matter how long you wait, or you have a retry loop already and want to know whether it can run up a bill.
The SDK already retries for you. Most of this page is about the cases where that is not enough, and one case where retrying is actively the wrong thing to do.
What you get without writing anything
The official SDKs retry transient failures with exponential backoff,
2 times by default, honoring the
retry-after header when the response carries one. Connection errors,
rate limits, and server errors are all covered.
client = anthropic.Anthropic(max_retries=8) # raise it
client = anthropic.Anthropic(max_retries=0) # or own the loop yourself
Two consequences people miss.
Timeouts are retried too. Wall-clock time for one call can reach the timeout multiplied by attempts plus one. With the default 10-minute timeout and default retries, a single call can occupy your process for half an hour before raising.
Retries are invisible. By the time an exception reaches you, several attempts have already happened. If you then wrap that call in your own retry loop, the counts multiply rather than add: five outer attempts around two inner retries is fifteen requests, not seven.
Which failures are worth retrying
Retrying a request that cannot succeed is not free: it costs latency, it can cost money, and under a rate limit it makes the condition worse.
| Status | Type | Retry? |
|---|---|---|
| 400 | invalid_request_error |
No. The request is wrong; sending it again keeps it wrong |
| 401 | authentication_error |
No |
| 402 | billing_error |
No |
| 403 | permission_error |
No |
| 404 | not_found_error |
No. Usually a model ID typo |
| 409 | conflict_error |
Yes, after resolving the conflict |
| 413 | request_too_large |
No. Requests cap at 32 MB |
| 429 | rate_limit_error |
Usually. See below |
| 500 | api_error |
Yes, with backoff |
| 504 | timeout_error |
Yes, and consider streaming |
| 529 | overloaded_error |
Yes, with backoff |
Catch these as a chain from most specific to least, not as one broad class. The SDK defines a distinct exception per status precisely so you can tell retryable from terminal:
try:
with client.messages.stream(...) as stream:
message = stream.get_final_message()
except anthropic.NotFoundError:
raise # a model ID typo: fail loudly, now
except anthropic.RateLimitError as e:
wait = e.response.headers.get("retry-after")
...
except anthropic.APIStatusError as e:
if e.status_code >= 500:
... # retry with backoff
else:
raise # other 4xx: terminal
except anthropic.APIConnectionError:
... # never reached the API; retry
A single except Exception here is the bug that produces a service which retries a malformed
request forty times and reports "the API is flaky".
The 429 that never succeeds
This is the case that justifies the whole page.
Two very different conditions return HTTP 429 with error type rate_limit_error:
An ordinary rate limit. You are sending faster than your limit allows. The response carries a
retry-after header. Waiting works. This resolves in seconds.
Your organization's monthly spend cap. API usage is paused until 00:00 UTC on the first of
next month. The response carries no retry-after header, and per
Anthropic's documentation, retrying — including the SDK's automatic retries — fails until access
resumes.
Tell them apart by error.details.error_code, which is
enforced_spend_limit_reached for the spend cap:
except anthropic.RateLimitError as e:
body = e.response.json().get("error", {})
code = body.get("details", {}).get("error_code")
if code == "enforced_spend_limit_reached":
# Retrying will not help before the month rolls over. Page a human.
raise SpendCapReached(body.get("message"))
retry_after = e.response.headers.get("retry-after")
...
There is a third variant worth knowing: a spend limit you set on your own organization or
workspace returns HTTP 400 invalid_request_error, not 429. A retry loop keyed on status code
will treat it as a malformed request and give up, which is the right outcome reached by the wrong
reasoning — and it means the message never surfaces as a billing problem in your logs.
Reading the headers instead of guessing
Every response carries headers reporting the limit, what is left, and when it replenishes. They
are prefixed anthropic-ratelimit-, and there are separate families for
requests, input tokens and output tokens.
raw = client.messages.with_raw_response.create(...)
remaining = raw.headers.get("anthropic-ratelimit-requests-remaining")
resets_at = raw.headers.get("anthropic-ratelimit-requests-reset")
The reset values are RFC 3339 timestamps rather than durations. Limits replenish continuously under a token-bucket algorithm rather than resetting on a fixed boundary, so the useful question is usually "how much headroom do I have now", not "when does the window roll".
Worth pinning, because secondary sources get it wrong: the prefix is
anthropic-ratelimit-. A header name from memory that no response ever
carries reads as zero remaining quota and throttles a service that was never rate-limited.
Backoff that is not hostile
If you write your own loop, three properties matter.
Respect the header. When retry-after is present it is not a
suggestion; earlier retries fail.
Exponential, with jitter. Without jitter, everything you throttled retries simultaneously and re-trips the limit. Jitter exists to break that synchronization, not to be polite.
A ceiling on attempts. Which brings us to the point of the section below.
import random, time
def backoff(attempt, retry_after=None, base=1.0, cap=60.0):
"""Seconds to wait before the next attempt."""
if retry_after is not None:
return float(retry_after)
return min(base * (2 ** attempt) + random.uniform(0, 1), cap)
Writing it, three times over
A retry loop that knows when to stop
Retry the failures worth retrying, and stop on the ones that will never succeed.
Worked through, every step explained
-
1
for attempt in range(max_attempts):ⓘA bounded loop, not
while True. The bound is the whole point: an unbounded retry loop is an unlimited spend authorization written in code. -
2
try: return call()The call itself. Everything below is about what to do when this raises.
-
3
except anthropic.RateLimitError as e:ⓘCatch the specific class, not Exception. The SDK defines one per status precisely so you can tell retryable from terminal.
-
4
if spend_cap(e): raiseⓘThe 429 that never succeeds. A spend cap carries no wait header and stays failing until the month rolls over, so retrying it burns the whole budget for nothing.
-
5
wait = e.response.headers.get('retry-after')ⓘWhen the API tells you how long to wait, that is not a suggestion. Earlier retries fail.
-
6
except anthropic.APIStatusError as e:Everything else with a status. Retry at or above 500; re-raise below it, because a malformed request stays malformed however many times you send it.
The same task, on your own
Do the whole thing yourself. Hover a line number for a hint.
Example source: data/examples/retry-loop.yaml, provenance
human. The two faded versions are generated from the first, so they
cannot drift from it. Notes live in
data/annotations/retry-loop.yaml; each is anchored to the text it explains and
the build fails if an anchor stops resolving.
Cost containment is a feature, not a nicety
A retry loop with no ceiling is an unbounded spend authorization written in code.
This is not hypothetical arithmetic. Each retry of a request that has already produced output is billed. A loop that retries until success, against a condition that will not resolve — the spend cap above, a permanently malformed request, a model ID that does not exist — spends money at whatever rate your backoff allows, for as long as the process lives.
The anti-pattern, stated plainly:
while True: # no
try:
return call()
except Exception:
time.sleep(5)
That loop cannot terminate on a terminal error, cannot distinguish a spend cap from a rate limit, and has no upper bound on cost. It is also, in the author's experience, the most common retry implementation in production code, because it looks correct at a glance.
The ceiling this site runs. This documentation site caps retries at 5 attempts, backing off from 1 second and multiplying by 2.
Those are not numbers written into this paragraph. They are read from the same key that the site's own live demo reads, so the discipline described here and the discipline practised here cannot drift apart. The eval suite fails the build if the documented ceiling and the enforced one disagree.
Which is the whole argument: a document that tells you to cap your retries, while the system serving it does not, is advice. A document that reads its number from the thing it is describing is a demonstration.