Module 01 · Lesson 10 of 13Free
Model API errors, rate limits and retries
- Level
- Intermediate
- Topic
- AI engineering fundamentals
- Reading time
- 17 min
When you finish this page you can
- Sort model API errors into the ones a retry can fix and the ones it can’t, from the status code and error type.
- Work out which rate limit a burst of calls hits first, from requests and tokens per minute.
- Configure the SDK’s retries and timeout, and add slower job-level retries with capped backoff and jitter instead of a second retry loop.
- Decide what a feature shows when retries run out, so a user never sees a broken result.
Builds on 1 lesson
Start with lesson 315 minVideo lesson · coming soon
Sale day: 9,000 summaries and a wall of 429s
Deskora is a help desk app. A shop’s support team uses it to answer customer emails from one shared inbox. Each customer problem becomes a ticket: one customer’s whole conversation with the support team, message by message. When a ticket gets long, a asks a model for a three-line summary, so the agent who picks it up can reply sooner.
At 10:00 one morning, hundreds of Deskora’s shops start a seasonal sale at the same time. Priya’s kitchenware shop is one of them. For six minutes, about 1,500 tickets a minute grow long enough to need a summary.
Tomás, the Deskora engineer who built the summaries, sees the error log fill with 429 rate_limit_error. Hidden among them are 404 not_found_error lines: a config change sent some jobs the model id claude-haiku-5.5 instead of claude-haiku-5-5.
At 10:40, Priya opens a 41-message ticket from a customer, Amara. The summary box at the top is empty. She reads all 41 messages anyway.
This lesson answers one question: which failures should Deskora retry, how should it retry them, and what does Priya see when the retries run out?
Deskora’s 120,000 tickets a day is a fact used across this course. The ticket sizes, the sale-day numbers, Deskora’s usage tier and the timings below are estimates for this lesson’s scenario.
Diagram not drawn yet: deskora-sale-day-summary-jobs.svg
Left to right: a job queue holding 9,000 summary jobs, fed at 1,500 jobs a minute; summary workers taking jobs; the Claude Messages API. Three dashed replies return from the API: about 625 a minute succeed with 200, about 875 a minute fail with 429 rate_limit_error, and jobs with the wrong model id fail with 404 not_found_error. A ticket database below the workers receives the summaries, and Priya’s browser reads a ticket whose summary box is empty.
Deskora’s workers send about 1,500 summary calls a minute. Its rate limit for Claude Haiku 5.5 is 5,000 requests a minute. Why do most calls still get a 429?Show answer
Two kinds of failure: wait it out or fix it
The model API answers a failed call with an HTTP status code and a JSON body. The body’s error.type names the problem, and request_id identifies the call for support 1:
{
"type": "error",
"error": {"type": "rate_limit_error", "message": "..."},
"request_id": "req_011CSHoEeqs5C35K2UUqR7Fy"
}Ask one question of each error: would the same request, sent again later, succeed? If yes, it is a retryable error, caused by load or a passing fault. If no, the request or the account is wrong.
These are the errors the Claude API documents, as of October 2026 1:
Status and error.type |
What it means | Retry? |
|---|---|---|
429 rate_limit_error |
Too many requests or tokens per minute | Yes, after waiting |
500 api_error |
Unexpected fault inside the provider | Yes, with backoff |
504 timeout_error |
The provider timed out while working on it | Yes |
529 overloaded_error |
The API is overloaded for all its users | Yes, with backoff |
| No status: connection error or your own | The request or reply was lost on the way | Yes |
400 invalid_request_error |
The request is malformed, or uses a setting the model rejects | No: fix the request |
401 authentication_error |
The is malformed, revoked or expired | No: fix the key |
402 billing_error / 403 permission_error |
Payment problem, or the key may not use this resource | No: fix the account |
404 not_found_error |
Wrong path, or an unknown model id | No: fix the config |
413 request_too_large |
Over the request size limit (128 MB on the Claude API) | No: send less |
429 with enforced_spend_limit_reached |
The monthly spend cap is used up | No: wait for the month or raise the cap |
One row needs care. A 429 usually means “slow down”, but the same status also arrives when Deskora’s monthly spend cap is reached. That 429 has no retry-after header, carries error.details.error_code: "enforced_spend_limit_reached", and keeps failing until the next month 2. A spend limit Deskora sets itself returns a 400 instead.
Retrying a fixable error is not harmless: the claude-haiku-5.5 jobs fail on every attempt, and each retry hides the bug longer.
Diagram not drawn yet: model-api-errors-retry-or-fix.svg
Two panels. Left panel, titled Wait and retry: 429 rate_limit_error, 500 api_error, 504 timeout_error, 529 overloaded_error, connection error or client timeout. Label: the same request can succeed later. Right panel, titled Fix, don’t retry: 400 invalid_request_error, 401 authentication_error, 402 billing_error, 403 permission_error, 404 not_found_error such as the wrong model id, 413 request_too_large, and 429 with enforced_spend_limit_reached. Label: the same request fails every time; alert a person.
To retry a 429 well, you need to know what is counted.
What the rate limit counts
caps how much work an organization may send. On the Claude Messages API, each model class has three limits: requests per minute (RPM), input tokens per minute (ITPM) and output tokens per minute (OTPM) 2. Exceed any one, and the call gets a 429 that says which limit it hit.
The limits work like a . Capacity refills continuously, up to the maximum, rather than resetting at the top of each minute. A limit of 60 requests a minute may also be enforced as one a second, so a short burst can fail even when the minute’s total is fine 2. The bucket mechanics are taught in Rate limiting algorithms.
For most current models, only uncached input tokens count toward ITPM, and only tokens actually produced count toward OTPM 2.
Assume Deskora is on the Build tier. As of October 2026, its limits for Claude Haiku 5.5 are 5,000 RPM, 5,000,000 ITPM and 1,000,000 OTPM 2.
A normal day
40,000 summaries ÷ 1,440 minutes ≈ 28 a minute; 28 × 6,900 input tokens ≈ 190,000 ITPM, about 4% of 5,000,000
40,000 of the 120,000 daily tickets get a summary, one in four of them long. The weighted average input is about 6,900 tokens.
Sale-day requests
1,500 requests a minute ÷ 5,000 RPM = 30% of the limit
The job runner starts each job as soon as it is queued.
Sale-day input tokens
1,500 × 8,000 tokens = 12,000,000 ITPM = 2.4 × the 5,000,000 limit
Assumed: sale-day threads average 8,000 input tokens.
Sale-day output tokens
1,500 × 500 tokens = 750,000 OTPM = 75% of the limit
Assumed: about 400 thinking tokens and a 100-token summary per call. Thinking counts as output.
What fits
5,000,000 ÷ 8,000 = 625 summaries a minute; 9,000 jobs ÷ 625 ≈ 15 minutes
About 875 calls a minute fail with a 429 while the burst lasts.
Input tokens run out first, while the request count sits at 30%. The provider will accept 625 summaries a minute however the calls are sent. Retries can’t raise that number; they only decide which jobs get the space, and when.
A sudden jump can also cause 429s: the API applies acceleration limits when usage rises sharply, and the docs advise ramping up gradually 1. A sale that starts at 10:00 sharp is that kind of jump.
A 429 from a rate limit usually carries a retry-after header: the number of seconds to wait, since earlier retries will fail 2. Every response also carries anthropic-ratelimit-* headers showing what remains. Slowing down by reading those headers is pacing, covered with queues in Provider rate limits, queues and fallbacks.
Waiting works, if the wait is long enough. Tomás’s first version didn’t wait long enough.
Retrying on top of retrying
Fifteen calls per job, then an empty summary
- Tomás’s job wraps each call in his own loop: up to 5 attempts, 1 second apart, catching every exception. If all 5 fail, it saves the summary, which is still an empty string, and marks the ticket as summarized.
- He doesn’t know that the anthropic SDK already retries each call twice before raising an error. A job that keeps failing makes up to 5 × 3 = 15 calls.
- At 10:00, 875 jobs a minute start failing. Their retries land in a bucket that is already empty, so they fail too. Every job sleeps exactly 1 second, so the retries arrive in waves.
- A job gives up within about a minute. The overload lasts about 15 minutes, so most failed jobs give up long before space returns.
- The
claude-haiku-5.5jobs also try 5 times each. Every attempt fails with a 404, and the lines are lost among thousands of 429s. - Each failed job saves an empty summary and marks the ticket done, so nothing ever tries again. Priya sees an empty box on Amara’s ticket at 10:40, long after the provider had room.
A common belief is that more retries make a job more reliable. Here they can’t. The bucket accepts 625 summaries a minute whether each job tries 3 times or 15. Extra attempts only add load and noise. What was missing was time: retries spread over the 15 minutes the backlog lasts, not over one minute.
Waiting longer between tries is the job of : each retry waits up to twice as long as the one before, up to a cap. picks a random delay inside that window, so the 875 jobs that failed together don’t all come back in the same second 5. Full jitter, caps and retry amplification are taught in Exponential backoff and jitter.
Diagram not drawn yet: summary-retry-timelines.svg
Two panels with the same time axis from 10:00 to 10:30. A shaded bar from 10:00 to 10:15 marks the overload, when the bucket is empty. Top panel, Before: one job makes 15 calls between 10:00:00 and about 10:01, all inside the shaded bar, all 429, then saves an empty summary. Bottom panel, After: the SDK makes 3 calls in a few seconds at 10:00, all 429; the job waits a random time up to 2 minutes, then up to 4, then up to 8; its fourth try lands, for example, at about 10:16, after the shaded bar, and succeeds.
Where the retries live
The SDK’s built-in retries cover connection errors, 408, 409, 429 and every status of 500 or above, which includes 529. They use a short exponential backoff and honour retry-after 3. You set two numbers on the client: max_retries (default 2) and timeout (default 10 minutes). Timed-out requests are retried too 3.
Where should the retries for a summary job live?
Only the SDK’s retries, with the defaults.
Survives a few seconds of trouble. In a 15-minute overload every failed job gives up within seconds, and the ticket gets no summary.
Your own retry loop around the SDK call.
Up to 15 calls per job, sent in waves; retries 404s and bad keys too; still gives up within about a minute. This was Tomás’s first version.
SDK retries for seconds, plus slow job-level retries for retryable errors only.
Short blips are absorbed in seconds. Long overloads are waited out over minutes, with 4 job-level retries of up to 2, 4, 8 and 16 minutes. Fixable errors fail at once and alert a person.
Why this choice
The two layers handle different lengths of trouble. The SDK retries within one call, so a dropped connection costs Priya nothing. The job queue retries across minutes, which matches how long a sale-day burst lasts. Each layer retries once; neither wraps the other in a loop. A summary is safe to repeat, because writing the same ticket’s summary twice changes nothing.
When the choice changes. If a person is waiting for the answer, as in a chat reply, minutes of retries are useless: keep the SDK’s retries, a short timeout and a clear message, and see Streaming responses for errors that arrive mid-stream. If job-level retries run every day, Deskora is over its limit by design. Then the fix is pacing, a higher tier or a fallback, not more retries.
Here is the job function. The calls it, and reschedules the job when it returns "later". It never stores an empty or cut-off summary.
import random
import anthropic
# Seconds-scale retries stay in the SDK. 60 s is far above a normal summary's time.
client = anthropic.Anthropic(max_retries=2, timeout=60.0)
SYSTEM = "Summarize the support ticket in exactly three lines."
MAX_JOB_ATTEMPTS = 5
TRY_LATER = (anthropic.InternalServerError, # 500, 503, 504 and other 5xx
anthropic.OverloadedError, # 529
anthropic.APIConnectionError) # includes APITimeoutError
def spend_cap_reached(err: anthropic.RateLimitError) -> bool:
body = err.body if isinstance(err.body, dict) else {}
details = body.get("error", {}).get("details") or {}
return details.get("error_code") == "enforced_spend_limit_reached"
def later(attempt: int) -> tuple[str, str | float]:
if attempt >= MAX_JOB_ATTEMPTS:
return ("failed", "gave up: leave the ticket without a summary")
window = min(30.0, 2.0 * 2 ** (attempt - 1)) # 2, 4, 8, 16 minutes, cap 30
return ("later", random.uniform(0, window)) # full jitter
def run_summary_job(thread: str, attempt: int) -> tuple[str, str | float]:
"""Returns ("done", summary), ("later", minutes) or ("failed", reason)."""
try:
response = client.messages.create(
model="claude-haiku-5-5", max_tokens=2000, system=SYSTEM,
messages=[{"role": "user", "content": thread}],
)
except anthropic.RateLimitError as err:
if spend_cap_reached(err):
return ("failed", "spend cap reached: pause all summary jobs")
return later(attempt)
except TRY_LATER:
return later(attempt)
except anthropic.APIStatusError as err: # 400, 401, 403, 404, 413
return ("failed", f"{err.status_code}: fix and alert, don't retry")
if response.stop_reason != "end_turn":
return ("failed", f"stop_reason={response.stop_reason}")
return ("done", "".join(b.text for b in response.content if b.type == "text"))The order of the except clauses matters. RateLimitError and InternalServerError are both kinds of APIStatusError, so they must come before it. The last check is the one from Calling a model API: a 200 with a stop_reason other than end_turn is not a finished summary.
Every retry that runs is billed
A retry is a new request. If it reaches the model, it is billed like any other call. A 429 is rejected before the model runs, so it produces no tokens. A call your client gives up on is different: the provider may finish the work and bill it, even though nobody reads the answer.
That makes the timeout a cost setting. Suppose Tomás had set timeout=10.0, and a long ticket’s summary takes about 12 seconds (an assumption for this example).
Calls per long summary
1 call + 2 SDK retries = 3 calls, every one timed out
Each call is cut off at 10 seconds, so the long ticket never gets a summary.
Cost per long ticket, if each abandoned call is billed
3 × (15,150 × $0.10 + 900 × $0.50) ÷ 1,000,000 = 3 × $0.001965 ≈ $0.0059
Claude Haiku 5.5 prices as of October 2026 4. Assumed: 800 thinking tokens and a 100-token summary, billed as output.
Per day
10,000 long tickets × $0.0059 ≈ $59, for no long summaries at all
Against about $19.65 a day when every long summary succeeds on its first call.
So set the timeout well above the slowest normal call: 60 seconds here. A completed summary costs the price per call times the billed calls it took; the next lesson builds that into the estimate.
When retries run out, show no summary
After 5 attempts, about 30 minutes in the worst case, the job stops. What should Priya see then?
| Option | What Priya sees | The risk |
|---|---|---|
| Show the error | “Summary failed: 429” | Noise she can do nothing about |
| Show an empty or cut-off summary | A blank box, or a line that ends mid-sentence | She trusts it, or waits for it |
| Show no summary | The ticket as it looked before summaries existed | She reads the thread, as she always did |
Deskora shows no summary. A missing summary costs Priya five minutes of reading. A wrong one can cost Amara a wrong reply. Calling a second provider when retries run out is a fallback, covered in the later lesson on queues and fallbacks linked above. A model that declines to answer is a different failure, covered in Refusals and fallbacks.
Each summary job moves through these states:
| State | Event | Next state | Priya sees |
|---|---|---|---|
| queued | A worker picks it up | running | No summary yet |
| running | "done" |
done | The summary |
| running | "later", attempts left |
waiting | No summary |
| waiting | The random delay passes | queued | No summary |
| running | "failed": a fixable error, or attempts used up |
failed | No summary |
A failed job never writes to the summary field. A fixable error also pages an engineer, because no amount of waiting fixes it. The wrong model id would have paged Tomás at 10:01.
Try it yourself
Send a call with the wrong model id and read the error. A rejected request produces no tokens. If you don’t have an API key, skip this.
curl -sS -i https://api.anthropic.com/v1/messages \
-H "content-type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2026-10-21" \
-d '{"model": "claude-haiku-5.5", "max_tokens": 200, "messages": [{"role": "user", "content": "Say hello."}]}'One run returned this, shortened; your request id and message will differ:
HTTP/2 404
request-id: req_011C...
content-type: application/json
{"type":"error","error":{"type":"not_found_error","message":"model: claude-haiku-5.5"},"request_id":"req_011C..."}What to look for:
- The status and
error.type: a 404 that no retry will fix. - Change the model to
claude-haiku-5-5and run it again. It costs at most about $0.0001: 200 output tokens × $0.50 per million, plus a few input tokens. - In that response, find
anthropic-ratelimit-input-tokens-remainingandanthropic-ratelimit-requests-remaining. They show how much of the bucket is left.
Where this shows up
- Design: a ticket summary feature puts these job states into the full feature, with the queue and the storage.
- Design: an AI customer support platform makes several calls per ticket, so one rate limit is shared by many features.
- Design: scaling a chat assistant to a million users meets rate limits with a person waiting on every call.
Retries, timeouts and thinking all change what a summary really costs and how long it takes. The next lesson, Estimating the cost and latency of a feature, puts them into one estimate before launch.
Common mistakes
- Retrying every exception. A 400, 401 or 404 fails on every attempt. Fail at once, and alert a person.
- Wrapping the SDK in another retry loop. The SDK already retries twice. Set
max_retriesandtimeouton the client instead. - Treating every 429 the same. The spend-cap 429 has no
retry-afterand carriesenforced_spend_limit_reached. It lasts until the next month. - Saving whatever came back. An empty string, an error message or a cut-off summary must never reach the summary field.
Check your understanding
After a deploy, every summary job fails with 401 authentication_error. The job queue retries each job 4 more times over 30 minutes. What is wrong with that?Show answer
On the first of the month, every call returns a 429 and the response has no retry-after header. What do you check before letting jobs retry?Show answer
Tomás raises max_retries from 2 to 10 to get through the next sale. Will more summaries succeed during the 15-minute burst?Show answer
Deskora moves its long tickets to a model that needs about 50 seconds per summary, and the client timeout stays at 60 seconds. Sale-day calls slow down by 30%. What happens?Show answer
Summary
- Retry only what can succeed later unchanged: 429 rate limits, 500, 504, 529, connection errors and timeouts. Fail at once on 400, 401, 402, 403, 404 such as a wrong model id, 413, and the spend-cap 429, and alert a person.
- Rate limits count requests, input tokens and output tokens per minute. Long prompts usually hit the input-token limit first, and capacity refills continuously, so a burst over the limit gets 429s with retry-after.
- The Python SDK already retries twice with backoff. Configure max_retries and timeout instead of adding a loop, and put slower retries, with capped backoff and jitter, in the job queue.
- Every retry that reaches the model is billed, and an abandoned call may be too, so a timeout that is too short multiplies cost and still fails.
- When retries run out, Deskora shows no summary and leaves the thread as it was, never an empty, wrong or cut-off one.