Diagnosing 429 Rate-Limit Errors and Quota Exhaustion Under Load
A 429 from the Claude API always means the same thing at the protocol level, you exceeded a limit, but the specific limit, the reason, and the fix are different every time. Diagnosing a 429 correctly, before you touch your retry logic, is what separates a five-minute fix from a repeat incident.
Summary
The Claude API enforces several independent rate limits at once: requests per minute, input tokens per minute, and output tokens per minute, each tracked separately per model.
A 429 response tells you a limit was hit, but the response headers tell you which one, and that detail changes what you should actually do about it.
Rate limits also come in two flavors that look identical from the client but require different fixes: a genuine account or organization quota ceiling, and a short burst of concurrent traffic that briefly exceeds your allocated throughput.
This page walks through reading the signal the API gives you, telling burst spikes apart from sustained overload, and building the minimal diagnostic logging that makes the next 429 storm a quick read instead of a guessing game.
It stops at diagnosis. Once you know which limit you're hitting and what shape the traffic is, the retry and backoff article covers what to build in response.
Recipe
Quick-reference recipe card - copy-paste ready.
import anthropic
client = anthropic.Anthropic()
try:
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Summarize this report."}],
)
except anthropic.RateLimitError as e:
headers = e.response.headers
print("retry-after:", headers.get("retry-after"))
print("requests remaining:", headers.get("anthropic-ratelimit-requests-remaining"))
print("input tokens remaining:", headers.get("anthropic-ratelimit-input-tokens-remaining"))
print("output tokens remaining:", headers.get("anthropic-ratelimit-output-tokens-remaining"))When to reach for this:
- Your application is intermittently returning 429s and you need to know whether it's a request-count, input-token, or output-token limit before choosing a fix.
- You suspect a burst of concurrent requests, rather than sustained volume, is tripping the limit.
- You're setting a baseline before adding retry and backoff logic, so you can measure whether the fix actually worked.
- You need to distinguish "we're out of quota for the billing period" from "we briefly exceeded our per-minute allocation."
Working Example
import logging
import time
import anthropic
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rate_limit_diagnostics")
client = anthropic.Anthropic()
def diagnose_rate_limit(exc: anthropic.RateLimitError) -> dict:
"""Extract a structured signature from a RateLimitError so it can be
logged, aggregated, and compared across incidents."""
headers = exc.response.headers
requests_remaining = headers.get("anthropic-ratelimit-requests-remaining")
input_tokens_remaining = headers.get("anthropic-ratelimit-input-tokens-remaining")
output_tokens_remaining = headers.get("anthropic-ratelimit-output-tokens-remaining")
retry_after = headers.get("retry-after")
if requests_remaining == "0":
limit_type = "requests_per_minute"
elif input_tokens_remaining == "0":
limit_type = "input_tokens_per_minute"
elif output_tokens_remaining == "0":
limit_type = "output_tokens_per_minute"
else:
limit_type = "unknown_or_org_quota"
signature = {
"limit_type": limit_type,
"retry_after_seconds": retry_after,
"request_id": exc.request_id,
"timestamp": time.time(),
}
logger.warning("claude.rate_limited", extra=signature)
return signature
def call_with_diagnostics(prompt: str) -> anthropic.types.Message | None:
try:
return client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
except anthropic.RateLimitError as e:
diagnose_rate_limit(e)
return None
if __name__ == "__main__":
call_with_diagnostics("Draft a status update for the incident channel.")What this demonstrates:
- Reading the
anthropic-ratelimit-*headers to determine which specific limit (requests, input tokens, or output tokens) drove the 429, rather than treating every 429 the same. - Capturing
retry-afterso you know how long the API itself thinks you should wait before trying again. - Logging a structured signature per event, so a burst of 429s produces an aggregable dataset instead of a wall of identical stack traces.
- Keeping the
request_idfor correlation with Anthropic support or your own RCA if the pattern recurs.
Deep Dive
How It Works
- The Claude API tracks rate limits per organization and per model, across three independent dimensions: requests per minute, input tokens per minute, and output tokens per minute.
- A request can be rejected for exceeding any one of these, even if the other two have headroom, so a single "rate limited" log line without the header detail tells you almost nothing.
- The
retry-afterheader is the API's own recommendation for how long to wait, expressed in seconds, and it accounts for the specific limit that was exceeded. - Quota ceilings (a hard cap tied to your account tier or spend limit) look identical to a transient per-minute limit in the exception type, but they don't clear on their own the way a per-minute limit does, they require a tier upgrade or a support conversation.
Rate Limit Signatures at a Glance
| Header | Meaning | What It Tells You |
|---|---|---|
anthropic-ratelimit-requests-remaining | Requests left in the current window | 0 means you're hitting the request-count ceiling |
anthropic-ratelimit-input-tokens-remaining | Input tokens left in the current window | 0 means large prompts or context are the driver |
anthropic-ratelimit-output-tokens-remaining | Output tokens left in the current window | 0 means high max_tokens or long generations are the driver |
retry-after | Seconds until the API expects you to retry | A short value (under a few seconds) usually means a burst; a long value often means sustained overload |
Bursty vs Sustained Traffic
- Bursty: a short spike, often caused by a batch job, a retry storm, or many users triggering an action at once (e.g. a deploy that resets client-side caches and causes a thundering herd of requests). The signature is a cluster of 429s in a narrow time window, followed by a clean recovery.
- Sustained: your steady-state traffic has grown past what your current tier or configured concurrency supports. The signature is a 429 rate that doesn't fully recover between windows, it degrades, recovers partially, then degrades again.
- Plot 429 occurrences over a rolling 5-minute window before deciding which one you're looking at. A single spike graph is not enough to tell burst from sustained; you need to see whether the baseline itself is climbing.
# A minimal way to tell burst from sustained: track the ratio of
# rate-limited calls to total calls over a rolling window and watch
# whether the baseline recovers between spikes.
from collections import deque
import time
class RateLimitTracker:
def __init__(self, window_seconds: int = 300):
self.window_seconds = window_seconds
self.events: deque[tuple[float, bool]] = deque()
def record(self, was_rate_limited: bool) -> None:
now = time.time()
self.events.append((now, was_rate_limited))
cutoff = now - self.window_seconds
while self.events and self.events[0][0] < cutoff:
self.events.popleft()
def rate_limited_ratio(self) -> float:
if not self.events:
return 0.0
limited = sum(1 for _, hit in self.events if hit)
return limited / len(self.events)Quota Ceilings vs Per-Minute Limits
| Signal | Per-Minute Limit | Account/Org Quota Ceiling |
|---|---|---|
| Recovery | Clears within the window (typically 60 seconds) | Does not clear until the billing period resets or the tier changes |
retry-after | Usually seconds | May be absent or not meaningfully useful |
| Fix | Backoff, concurrency control, request batching | Request a tier increase, review spend, add capacity in the console |
Gotchas
-
Treating every 429 as the same problem. Logging "rate limited" without the header breakdown hides whether it's a request-count, input-token, or output-token limit. Fix: always log the three
anthropic-ratelimit-*-remainingheaders alongside the exception. -
Confusing a burst with sustained overload. A single alert firing during a five-minute spike can trigger a capacity upgrade that doesn't actually fix a recurring burst pattern. Fix: track the rolling ratio over at least 15-30 minutes before concluding it's sustained.
-
Ignoring
retry-after. Rolling your own fixed backoff interval when the API already tells you how long to wait wastes retries and can make the limit worse. Fix: read and respectretry-afterwhen present; treat it as a floor, not a ceiling. -
Not separating limits by model. If your traffic calls both Sonnet and Haiku, a 429 on one model's limit doesn't mean the other model is also constrained. Fix: tag diagnostic logs with the
modelparameter used in the request that failed. -
Diagnosing from client-side logs alone. Client-side timing can make a slow, healthy response look identical to a queued retry after a 429. Fix: always capture the actual exception and its headers, not just elapsed request time.
-
Assuming quota exhaustion will self-heal like a per-minute limit. Waiting and retrying against a genuine account quota ceiling just produces more 429s. Fix: check whether
retry-afteris present and short before assuming this is transient; if it's missing or unusually long, check your account's quota status in the console.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Manual header inspection (this article) | You need to understand the exact cause before changing behavior | You already know the cause and just need resilience, use the retry/backoff article instead |
| A dedicated rate-limit dashboard/metrics pipeline | You have recurring 429 incidents and need historical trend data | You're diagnosing a first-time or one-off spike, a dashboard is overkill for a single incident |
| Requesting a quota/tier increase | Diagnosis confirms sustained overload against a real quota ceiling | The pattern is bursty, a higher ceiling just delays the same problem |
FAQs
What's the difference between a rate limit and a quota?
- A rate limit is a rolling per-minute ceiling on requests, input tokens, or output tokens, tracked per model.
- A quota is typically an account or organization-level cap tied to your usage tier or spend limit, and does not reset every minute.
How do I know which specific limit caused a given 429?
Check the anthropic-ratelimit-requests-remaining, anthropic-ratelimit-input-tokens-remaining, and anthropic-ratelimit-output-tokens-remaining response headers. Whichever one reads 0 at the time of the failed request is the limit that was hit.
Is `retry-after` always present on a RateLimitError?
It's present in the common case of a per-minute limit being exceeded. Treat its absence as a signal worth investigating further, since it can indicate a different kind of restriction, such as an account-level issue, rather than a simple transient limit.
Can I be rate limited on output tokens even with a short prompt?
Yes. Output token limits are driven by max_tokens and how much the model actually generates, not by prompt length. A short prompt with a high max_tokens and a long generation can exhaust the output-token limit on its own.
How long should I observe before deciding a 429 pattern is sustained rather than bursty?
- A single window is not enough; use at least 15-30 minutes of rolling data.
- Look for whether the rate-limited ratio returns to near zero between spikes (bursty) or stays elevated (sustained).
Does every model share the same rate limit?
No. Limits are tracked per model, so Claude Sonnet 5 and Claude Haiku 4.5 traffic are governed by separate limit buckets even within the same account.
Should I log every successful call, or only failures?
Log at minimum the remaining-capacity headers on every call, success or failure, if you want to see a limit approaching before it's actually hit. Logging only failures means you always find out after the fact.
What does it mean if `retry-after` keeps growing across consecutive 429s?
That's a strong signal of sustained overload, not a burst. A burst typically produces a retry-after that shrinks or stays flat as the window clears; a growing value suggests the underlying traffic hasn't backed off.
Is a 429 always the caller's fault?
Not necessarily. A sudden legitimate traffic increase, a client-side retry loop amplifying load, or a genuine account-tier ceiling that hasn't been raised to match growth can all produce 429s without any single bad request being at fault.
Can concurrent requests from multiple processes make diagnosis harder?
Yes. If several processes or workers share one API key, rate limit state is shared across all of them, so a spike in one worker's logs may actually be caused by a different worker entirely. Centralize your rate-limit logging across processes when possible.
What should I do immediately after confirming this is a genuine quota ceiling, not a transient burst?
Check your account's usage and tier limits in the Anthropic console, and consider whether recent traffic growth justifies a tier increase, rather than continuing to retry against a ceiling that won't clear on its own.
Where do I go after diagnosing the specific limit and traffic pattern?
Once you know which limit is being hit and whether it's bursty or sustained, the retry and backoff article covers the resilience patterns, jittered exponential backoff, circuit breakers, and idempotent retries, that address each case.
Related
- Understanding Why Claude Agents Fail in Production - where 429s fit in the broader failure taxonomy.
- Troubleshooting & Reliability Basics - the logging and exception-handling foundation this article builds on.
- Retry and Exponential Backoff Strategies for Transient Claude API Failures - what to build once you've confirmed the cause and pattern.
- Root-Cause-Analysis Template for Agent Failures - documenting a 429 incident once it's resolved.
- Troubleshooting & Reliability Best Practices - the broader checklist this diagnosis feeds into.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, pricing, and SDK versions move quickly - verify current specifics at platform.claude.com/docs before relying on them.