Claude API Rate Limit Exceeded: Error Handling Guide
A "rate limit exceeded" error from the Claude API means you've hit one of Anthropic's throttling thresholds — requests per minute, tokens per minute, or concurrent connections — and the API is temporarily rejecting new calls until the window resets. The fix isn't to panic-retry as fast as possible; it's to detect the specific error, back off correctly, and architect your request flow so you rarely hit the ceiling in the first place.
This article covers what the error actually looks like, how to handle it correctly in code, and how to design around it so it stops being a recurring production incident.
What a Rate Limit Error Looks Like
When you exceed a limit, the API returns an HTTP 429 Too Many Requests status with a JSON error body similar to:
{
"type": "error",
"error": {
"type": "rate_limit_error",
"message": "Number of request tokens has exceeded your per-minute rate limit."
}
}
Three distinct limit types can trigger this, and the message usually tells you which one:
- Requests per minute (RPM) — too many separate API calls
- Input/output tokens per minute (TPM) — your payloads are large or frequent
- Concurrent requests — too many requests in flight at once
The response usually includes retry-after style guidance in headers, which you should read programmatically rather than guessing a delay.
Step 1: Detect the Error Correctly
Don't treat every non-2xx response the same way. A 429 should be handled differently from a 400 (bad request, don't retry) or a 500 (server error, retry with caution).
async function callClaude(payload) {
const res = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify(payload),
});
if (res.status === 429) {
const retryAfter = res.headers.get("retry-after");
throw new RateLimitError(retryAfter ? Number(retryAfter) : null);
}
if (!res.ok) {
throw new Error(`Claude API error: ${res.status}`);
}
return res.json();
}
Keeping a dedicated RateLimitError class makes it easy to branch your retry logic without re-parsing status codes everywhere.
Step 2: Implement Exponential Backoff With Jitter
Fixed-delay retries cause synchronized retry storms when many requests fail at the same moment. Exponential backoff with jitter spreads retries out and respects the server's hint if one is provided.
async function withRetry(fn, { maxAttempts = 5 } = {}) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
if (!(err instanceof RateLimitError) || attempt === maxAttempts) {
throw err;
}
const base = err.retryAfterSeconds
? err.retryAfterSeconds * 1000
: 2 ** attempt * 500;
const jitter = Math.random() * 300;
await new Promise((r) => setTimeout(r, base + jitter));
}
}
}
A few rules worth following:
- Always cap
maxAttempts— infinite retries on a sustained outage just burns time and user patience. - Prefer the server's
retry-aftervalue over your own guess whenever it's present. - Log every retry with the attempt number and delay so you can see rate-limit pressure in your metrics, not just in error rates.
Step 3: Queue and Throttle Proactively
Reactive retries treat rate limiting as an exception. A more durable fix is to never send requests fast enough to trigger it. A simple token-bucket or sliding-window limiter on the client side keeps you under the ceiling:
class RequestQueue {
constructor(maxPerMinute) {
this.max = maxPerMinute;
this.timestamps = [];
}
async schedule(fn) {
const now = Date.now();
this.timestamps = this.timestamps.filter((t) => now - t < 60000);
if (this.timestamps.length >= this.max) {
const waitMs = 60000 - (now - this.timestamps[0]);
await new Promise((r) => setTimeout(r, waitMs));
}
this.timestamps.push(Date.now());
return fn();
}
}
This is especially important for batch jobs — summarizing thousands of documents, running evals, or backfilling embeddings — where it's tempting to fire everything concurrently. Throttle at the queue level and you'll finish the job reliably instead of fighting 429s the whole way through.
Step 4: Reduce Token Pressure, Not Just Request Count
TPM limits are often hit by oversized prompts, not request volume. Before adding more retry logic, check whether you're:
- Sending full conversation history on every turn when a summary would do
- Including large system prompts repeatedly instead of caching context where possible
- Requesting unnecessarily long outputs with high
max_tokensvalues
Trimming payload size is often a bigger win than any retry strategy, since it reduces how often you touch the limit at all.
Step 5: Separate Limits by Workload
If one background job (e.g., bulk classification) can starve your user-facing chat feature of rate limit headroom, split them. Running separate API keys or separate rate budgets per workload — one for interactive, latency-sensitive calls and one for batch — prevents a backlog job from causing user-visible errors.
This is one of the reasons teams move off raw provider keys and onto a gateway layer: it's easier to enforce per-workload quotas, see usage broken down by key, and avoid one misbehaving script taking down the whole integration. SubToAPI sits in front of your Claude access and issues separate sub_live_ application keys per service or team, along with per-key usage metadata, so a batch job and a production chatbot aren't drawing from the same invisible bucket. It won't eliminate rate limits — those are enforced upstream — but it gives you the visibility and separation to debug and distribute load sensibly. See the quickstart and messages API docs for how keys and requests map together.
Step 6: Monitor Before It Becomes an Incident
Track your 429 rate as a first-class metric, not just an error log line. A sudden spike usually means either traffic growth outpaced your plan, a deploy introduced a retry loop bug, or a batch job leaked into your interactive queue. Alerting on 429 rate (not just raw error count) catches rate-limit problems before they turn into user complaints.
Checklist
- Detect
429/rate_limit_errorexplicitly, don't lump it with generic errors - Use exponential backoff with jitter, respecting
retry-afterwhen present - Cap retry attempts and log every retry
- Add a client-side request queue/throttle to stay under limits proactively
- Reduce prompt and output size to ease TPM pressure
- Separate quotas or keys per workload so batch jobs don't starve production traffic
- Alert on 429 rate as a leading indicator, not a lagging one
FAQ
Does retrying immediately after a 429 ever work? Rarely. If you're over a per-minute limit, the window needs to roll forward before capacity frees up. Immediate retries just add to the pressure and risk triggering stricter throttling.
Should backoff logic differ between RPM, TPM, and concurrency errors? The retry mechanics are the same, but the fix differs: RPM errors need request throttling, TPM errors need smaller payloads, and concurrency errors need a lower in-flight request cap. Read the error message to know which lever to pull.
Can a gateway or proxy eliminate rate limit errors entirely? No — the limits are enforced by the upstream provider based on your account's plan. A gateway like SubToAPI can isolate workloads with separate keys and give you clearer per-key usage data, which helps you diagnose and distribute load, but the underlying limits still apply.