Handling Claude API errors: 429, 529, spend caps and rate limits
TL;DR
Not every error should be retried. A 429 can mean rate limiting (with retry-after) or a spend cap (no retry until next month). Prompt caching raises effective throughput because cache reads don't count toward ITPM on most models.
An app calling the Claude API runs fine on a dev machine, then in production it occasionally gets a 429, sometimes a 529, and one day the whole system is "dead" until the start of next month and nobody understands why. Most of these incidents come from treating every error the same: retry everything, or retry nothing. This post organises the error codes, how rate limits work and the correct handling per Anthropic's official docs.
Error codes: which to retry, which not
| HTTP | error.type | Cause | Retry? |
|---|---|---|---|
| 400 | invalid_request_error | Malformed request or content; also used when you hit a spend limit you set yourself | No, fix the request |
| 401 | authentication_error | Wrong, revoked or expired API key | No |
| 402 | billing_error | Billing problem | No |
| 403 | permission_error | Key lacks permission for the resource | No |
| 404 | not_found_error | Wrong endpoint or ID | No |
| 413 | request_too_large | Exceeds size limits (Messages 32 MB, Batch 256 MB, Files 500 MB) | No, split it |
| 429 | rate_limit_error | Rate limit exceeded, or tier spend cap reached | Yes, per retry-after (except spend cap) |
| 500 | api_error | Internal error | Yes, exponential backoff |
| 504 | timeout_error | Processing took too long | Yes; consider switching to streaming |
| 529 | overloaded_error | The API is overloaded generally, not your fault | Yes, backoff |
Errors always return JSON with error.type, error.message and request_id. Every response also has a request-id header: always log it, it's the first thing support asks for.
How rate limits work
- Three limits per model: RPM (requests/minute), ITPM (input tokens/minute), OTPM (output tokens/minute). Exceeding any one returns a 429.
- Token bucket: capacity refills continuously rather than resetting on the minute. 60 RPM may be enforced as 1 request/second, so short bursts can hit 429 even if the per-minute total isn't exceeded.
- By tier: organisations are placed in tiers automatically based on usage history. New organisations may start in an Evaluation tier with lower limits.
- Acceleration limits: sudden traffic spikes can get 429s even below the nominal limit. Ramp up gradually.
Prompt caching = a free limit increase
On most models, cache_read_input_tokens don't count toward ITPM; only input_tokens and cache_creation_input_tokens do. The docs give an example: with a 2M ITPM limit and an 80% cache hit rate, you can effectively process about 10M input tokens per minute. OTPM only counts tokens actually generated; a high max_tokens doesn't drain the limit.
The big trap: 429 from the spend cap
Each tier has a monthly spending cap. When you hit it, the API returns a 429 with the same error.type as a rate limit, but:
- There's no
retry-afterheader, anderror.details.error_codeisenforced_spend_limit_reached. - Retrying is pointless until 00:00 UTC on the 1st of next month, unless you move up a tier.
If your code only looks at the 429 status and retries forever, you burn CPU and logs while users wait indefinitely. Distinguish by error_code and alert immediately.
Suggested handling logic
try:
resp = client.messages.create(...)
except anthropic.RateLimitError as e:
body = e.body or {}
code = body.get("error", {}).get("details", {}).get("error_code")
if code == "enforced_spend_limit_reached":
alert_oncall("Spend cap reached"); raise # don't retry
raise # let the SDK/queue retry
except (anthropic.APIStatusError) as e:
log.error("claude_error", status=e.status_code,
request_id=e.response.headers.get("request-id"))
raise
The official SDKs already retry twice automatically with exponential backoff for connection errors, 429s and 5xx, honouring retry-after. Tune with max_retries. Don't wrap an unbounded manual retry loop around it, or you'll multiply the number of attempts.
Monitor with headers
Every response has anthropic-ratelimit-requests-remaining, anthropic-ratelimit-input-tokens-remaining, anthropic-ratelimit-output-tokens-remaining and the matching -reset headers (RFC 3339 format). Record them in metrics to see when you're about to hit the ceiling.
How to apply it
- Dev: Catch errors by SDK class, not by comparing message strings. Log
request-id. Use streaming for long requests instead of raising timeouts. - Tech Lead: Route high-volume jobs through a queue with bounded concurrency; move non-urgent jobs to the Batch API. Enable prompt caching for system prompts and repeated documents to raise throughput.
- PM: Set a spend limit per workspace in the Console and alert at 80% so you don't get "switched off" mid-month.
Sources & references
- Claude API errors — Claude Docs
- Rate limits — Claude Docs