Claude API Rate Limiting Best Practices
Rate limiting is the thing that quietly breaks production Claude integrations. Everything works fine in development with a handful of requests, then you ship to users and start seeing 429 responses, stalled queues, and unpredictable latency spikes. The fix isn't to "request a higher limit" — it's to build your client code so that rate limits are an expected, handled condition rather than an exception.
This article covers the concrete practices that keep a Claude-powered app stable under load: understanding what limits apply to you, implementing retry logic correctly, controlling concurrency, queuing requests, and monitoring usage before you hit a wall. These patterns apply whether you're calling Claude directly or through a proxy layer like SubToAPI.
Understand what's actually being limited
Claude API rate limits typically operate on multiple dimensions simultaneously:
- Requests per minute (RPM) — how many API calls you can make
- Tokens per minute (TPM) — input + output tokens processed, often split by model
- Concurrent requests — how many requests can be in-flight at once, especially relevant for streaming
Hitting any one of these returns a 429 status, usually with a retry-after header. The mistake most teams make is only handling RPM in their retry logic and ignoring TPM — so a few large-context requests silently throttle the whole app even though request count looks fine.
Before writing retry code, instrument your requests to log token usage per call. You can't tune backoff intelligently if you don't know whether you're RPM-bound or TPM-bound.
Implement exponential backoff with jitter
The standard, correct response to a 429 is exponential backoff with jitter — not a fixed delay, and not immediate retry.
async function callWithBackoff(fn, maxRetries = 5) {
let attempt = 0;
while (true) {
try {
return await fn();
} catch (err) {
if (err.status !== 429 && err.status !== 503) throw err;
if (attempt >= maxRetries) throw err;
const retryAfter = Number(err.headers?.['retry-after']);
const base = retryAfter ? retryAfter * 1000 : 2 ** attempt * 500;
const jitter = Math.random() * 300;
await new Promise((r) => setTimeout(r, base + jitter));
attempt++;
}
}
}
Key details that matter in practice:
- Respect
retry-afterwhen present. It's more accurate than a guessed backoff curve. - Add jitter. Without it, retries from multiple clients synchronize and hit the API in bursts, which just causes another wave of 429s.
- Cap retries. Five attempts is usually enough. Beyond that, fail fast and surface an error to the caller — infinite retry loops hide real problems.
- Treat 503s the same way. Transient server errors benefit from the same backoff strategy as rate limits.
Control concurrency, don't just react to errors
Backoff handles failures after they happen. The better approach is preventing most of them by controlling how many requests you send concurrently in the first place.
A simple semaphore-based limiter keeps you under your concurrency ceiling without external dependencies:
class ConcurrencyLimiter {
constructor(max) {
this.max = max;
this.active = 0;
this.queue = [];
}
async run(fn) {
if (this.active >= this.max) {
await new Promise((resolve) => this.queue.push(resolve));
}
this.active++;
try {
return await fn();
} finally {
this.active--;
this.queue.shift()?.();
}
}
}
const limiter = new ConcurrencyLimiter(8);
const result = await limiter.run(() => callClaude(prompt));
Tune the max concurrency to sit comfortably under your actual limit, not at it — leave headroom for other parts of your system (webhooks, background jobs, other users) competing for the same quota.
Queue bursty workloads instead of firing them all at once
If your app has bursty patterns — batch summarization jobs, bulk document processing, a spike of user activity — don't send everything at once and let backoff clean up the mess. Push work into a queue (even an in-memory one for moderate volume, or Redis/BullMQ for anything persistent) and drain it at a controlled rate.
This turns a rate limit problem into a throughput problem, which is much easier to reason about: if you know your TPM budget and your average tokens per request, you can calculate exactly how fast the queue should drain and set expectations for job completion time instead of guessing.
Separate retry logic from business logic
A common anti-pattern is scattering try/catch rate-limit handling across every call site. Centralize it instead — one wrapper function or middleware layer that all Claude calls go through. This makes it trivial to:
- Adjust backoff strategy globally when you change plans or models
- Log every rate-limit event in one place for monitoring
- Swap providers or add a proxy layer without touching call sites
This is one of the reasons teams put a layer like SubToAPI in front of their Claude usage: it turns your Claude access into a standard HTTPS API with sub_live_... application keys, so rate-limit handling, streaming, and usage metadata are consistent across every service that calls it, instead of each team reimplementing backoff logic independently. See the quickstart and messages docs for the request shape.
Monitor before you hit the ceiling
Reactive handling (backoff, retries) should be your safety net, not your primary strategy. Track these metrics continuously:
- 429 rate as a percentage of total requests — should be near zero in steady state
- Token consumption trend by day/hour, broken down by endpoint or feature
- P95 latency including retry time, not just the happy path
If 429 rate is consistently above low single digits, you're under-provisioned or your concurrency limiter is too loose — fix the root cause rather than tuning backoff harder. If you're using SubToAPI, usage metadata is returned with every response, which makes it straightforward to build this monitoring without a separate instrumentation pass — check the docs for the metadata fields available.
Design prompts to reduce token pressure
Rate limiting isn't purely an infrastructure problem — token-heavy prompts consume your TPM budget faster and push you toward limits sooner. A few habits that reduce pressure without hurting output quality:
- Trim unnecessary context (full conversation history when only the last few turns matter)
- Use prompt caching where supported for repeated system prompts or large static context
- Prefer concise system prompts over verbose ones restating the same instructions
- Stream responses so you can cancel early if the output goes off track, instead of paying for a full generation you'll discard
Questions
Does exponential backoff guarantee my requests eventually succeed? No. It reduces failed requests significantly but if you're structurally over your TPM/RPM budget, retries will keep failing. Backoff buys time during temporary spikes; sustained overload needs concurrency control or a higher-tier plan.
Should I implement rate limiting client-side or rely on server 429s? Both. Client-side concurrency limits prevent most 429s from happening. Server-side retry handling catches the ones that slip through, including limits you didn't anticipate (shared quota across team members, for example).
What's the easiest way to get consistent rate-limit handling across multiple apps using Claude? Centralize your Claude access behind one API layer with shared keys and logging instead of each app implementing its own retry logic. SubToAPI does this with per-application sub_live_... keys and shared usage visibility — see /pricing for plan details and /signup to start a trial.