A 429 from the Claude API means you crossed one of Anthropic's per-minute limits — requests, input tokens, or output tokens. The fix is rarely "wait longer": it's honoring retry-after, adding jittered backoff, and smoothing bursts. Here is the complete playbook.
The error
{
"type": "error",
"error": {
"type": "rate_limit_error",
"message": "Number of request tokens has exceeded your per-minute rate limit"
}
}Causes and fixes at a glance
| Cause | Fix |
|---|---|
| Requests-per-minute (RPM) limit hit | Queue requests client-side; honor the retry-after header before retrying. |
| Input-tokens-per-minute (ITPM) hit — large prompts, few requests | Trim retrieved context, enable prompt caching so cached tokens stop counting against you on supported plans. |
| Output-tokens-per-minute (OTPM) hit | Set max_tokens realistically — OTPM is often the first ceiling for long generations. |
| Burst traffic (cron fires everything at :00) | Add jitter to schedules; spread batch jobs across the minute. |
Read the response before retrying
Anthropic returns a retry-after header with the seconds to wait, and the error message names which limit you crossed. Retrying instantly without reading it is how a single 429 becomes a 429 storm.
Add exponential backoff with jitter
Retry only retryable statuses (429, 500, 529), never auth or validation errors. This snippet works unchanged against Anthropic directly or any OpenAI-compatible endpoint:
import time, random
from openai import OpenAI, APIStatusError
client = OpenAI(base_url="https://api.kunavo.com/v1", api_key="sk-kunavo-...")
def with_backoff(fn, max_retries=5):
for attempt in range(max_retries):
try:
return fn()
except APIStatusError as e:
if e.status_code not in (429, 500, 529):
raise # don't retry auth/validation errors
retry_after = e.response.headers.get("retry-after")
delay = float(retry_after) if retry_after else min(2 ** attempt, 30)
time.sleep(delay + random.uniform(0, 0.5)) # jitter avoids herds
raise RuntimeError("retries exhausted")
resp = with_backoff(lambda: client.chat.completions.create(
model="claude-sonnet-4-6",
messages=[{"role": "user", "content": "ping"}],
max_tokens=32,
))
print(resp.choices[0].message.content)Reduce the tokens, not just the requests
If the message says token limits, backoff alone won't save you. Cut retrieved chunks, cap max_tokens, and turn on prompt caching — a large stable system prompt at 10% of the input rate also relieves ITPM pressure.
If you're calling through Kunavo
Calling Claude through Kunavo doesn't magically remove rate limits, but it changes the failure economics: failed requests (including 429s) are never billed, and the per-key usage dashboard shows exactly which key and model is bursting so you can smooth it. The same backoff snippet above works as-is — just the base_url differs.
FAQ
Does upgrading my Anthropic tier remove 429s?
Higher tiers raise the per-minute ceilings, so 429s get rarer, but any fixed ceiling can be hit by a burst. Production code needs backoff regardless of tier.
Should I retry a 429 immediately?
No — honor the retry-after header (or use exponential backoff with jitter if it's absent). Immediate retries extend the rate-limited window and can escalate into a longer lockout.
Do failed 429 requests cost money?
Anthropic doesn't charge for rejected requests, and neither does Kunavo — failed requests are never billed. The cost of a 429 is latency, not dollars.
More error semantics live in the error reference; getting a key takes a minute via sign up and the authentication docs.