lumo

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

  1. Sort model API errors into the ones a retry can fix and the ones it can’t, from the status code and error type.
  2. Work out which rate limit a burst of calls hits first, from requests and tokens per minute.
  3. Configure the SDK’s retries and timeout, and add slower job-level retries with capped backoff and jitter instead of a second retry loop.
  4. Decide what a feature shows when retries run out, so a user never sees a broken result.
  1. Calling a model API: request, response and cost15 min

Video 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.

Sale day at 10:00. The job queue fills faster than the provider accepts calls, and a config bug hides among the 429s.
Check yourselfAnswer in your head first, then reveal
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 answerHide answer
Because requests are not the only thing counted. The provider also limits input tokens per minute, and 1,500 calls of about 8,000 tokens each is 12 million input tokens a minute, more than twice Deskora’s 5 million limit. Not “the request count is wrong”: the token limit is hit long before the request limit.

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:

json
{
  "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.

Ask one question of every error: would the same request succeed later? Left, wait and retry. Right, fix the request or the account, and alert someone.

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

  1. 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.
  2. 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.
  3. 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.
  4. A job gives up within about a minute. The overload lasts about 15 minutes, so most failed jobs give up long before space returns.
  5. The claude-haiku-5.5 jobs also try 5 times each. Every attempt fails with a 404, and the lines are lost among thousands of 429s.
  6. 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.

Fast retries all fall inside the overload. Retries spread over minutes, with jitter, land after it.

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.

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.

python
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.

bash
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
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-5 and 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-remaining and anthropic-ratelimit-requests-remaining. They show how much of the bucket is left.

Where this shows up

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_retries and timeout on the client instead.
  • Treating every 429 the same. The spend-cap 429 has no retry-after and carries enforced_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

Check yourselfAnswer in your head first, then reveal
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 answerHide answer
A 401 means the key is malformed, revoked or expired, so every retry fails the same way and only delays the alert. The job should fail at once and page an engineer. Not “the provider is busy”: busy shows up as 429 or 529, not 401.
Check yourselfAnswer in your head first, then reveal
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 answerHide answer
Check error.details.error_code. If it is enforced_spend_limit_reached, the monthly spend cap is used up and retries fail until the next month or a higher tier, so pause the jobs and alert. Not a normal rate limit: those usually carry retry-after and clear within seconds.
Check yourselfAnswer in your head first, then reveal
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 answerHide answer
Hardly. The provider still accepts about 625 summaries a minute, and the SDK’s short backoff spends 10 retries in well under 15 minutes. More retries add load, not capacity; job-level retries spread over minutes, or pacing, are what help.
Check yourselfAnswer in your head first, then reveal
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 answerHide answer
Calls start taking about 65 seconds, so they time out, and each one is retried twice by the SDK, so the same summary may be billed up to three times and still fail. Raise the timeout well above the slowest normal call, or stream long calls.

Summary

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. When retries run out, Deskora shows no summary and leaves the thread as it was, never an empty, wrong or cut-off one.

Report a mistake

Tell us what’s wrong in this lesson and we’ll fix it.