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.
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.
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."
import loggingimport timeimport anthropiclogging.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 signaturedef 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 Noneif __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-after so 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_id for correlation with Anthropic support or your own RCA if the pattern recurs.
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-after header 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.
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 dequeimport timeclass 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)
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-*-remaining headers 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 respect retry-after when 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 model parameter 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-after is present and short before assuming this is transient; if it's missing or unusually long, check your account's quota status in the console.
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.
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 anthropic Python 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.
Reviewed by Chris St. John·Last updated Jul 19, 2026