Claude API Rate Limiting Best Practices for Node.js
Claude API rate limits are enforced per-minute on requests, input tokens, and output tokens, and they scale with your account tier. When your Node.js app exceeds any of those limits, Anthropic returns a 429 Too Many Requests response with a retry-after header. Handling this correctly — instead of letting it crash a request or silently drop a user message — is the difference between a flaky integration and a production-grade one.
This article covers the concrete Node.js patterns that work: exponential backoff with jitter, client-side concurrency limiting, request queuing, and token-aware throttling. These apply whether you're calling Anthropic directly or through a proxy like SubToAPI, which forwards the same rate-limit headers so your retry logic doesn't need to change.
Understand what's actually being limited
Anthropic enforces three separate limits simultaneously:
- Requests per minute (RPM) — raw call count
- Input tokens per minute (ITPM) — tokens you send in
- Output tokens per minute (OTPM) — tokens generated back
A burst of short requests can hit RPM while leaving token budget untouched, and a single large-context call can blow through ITPM on its own. Any effective rate-limiting strategy has to account for all three, not just count requests.
Every response includes headers like anthropic-ratelimit-requests-remaining and anthropic-ratelimit-tokens-remaining. Reading these proactively — rather than waiting for a 429 — lets you throttle before you get rejected.
Exponential backoff with jitter
The baseline pattern: on a 429 or 5xx, wait, then retry, increasing the wait each time and adding randomness so multiple concurrent requests don't retry in lockstep.
async function callClaude(payload, attempt = 0) {
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 || res.status >= 500) {
if (attempt >= 5) throw new Error('Max retries exceeded');
const retryAfter = res.headers.get('retry-after');
const base = retryAfter ? Number(retryAfter) * 1000 : 2 ** attempt * 500;
const jitter = Math.random() * 300;
await new Promise((r) => setTimeout(r, base + jitter));
return callClaude(payload, attempt + 1);
}
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return res.json();
}
Always prefer retry-after over a fixed schedule when it's present — it reflects the actual window remaining, not a guess.
Limit concurrency before you limit retries
Backoff handles failures after they happen. Concurrency control prevents them in the first place. If your app fires 50 Claude calls at once from a queue processor or batch job, you'll hit RPM regardless of how good your retry logic is.
Use a simple semaphore instead of letting Promise.all fire everything simultaneously:
function createLimiter(maxConcurrent) {
let active = 0;
const queue = [];
const run = async (fn) => {
if (active >= maxConcurrent) {
await new Promise((resolve) => queue.push(resolve));
}
active++;
try {
return await fn();
} finally {
active--;
queue.shift()?.();
}
};
return run;
}
const limit = createLimiter(5);
const results = await Promise.all(
jobs.map((job) => limit(() => callClaude(job.payload)))
);
Tune maxConcurrent to your account's actual RPM tier, not a guess — check your dashboard limits and leave headroom for retries.
Token-aware throttling, not just request counting
Request-count limiting misses the bigger problem: large prompts. If you're summarizing documents or passing long conversation history, a handful of requests can consume your entire ITPM budget. Estimate token usage before sending, and throttle on that instead of (or in addition to) request count.
A rough rule of thumb is 4 characters per token for English text. It's not exact, but it's good enough to decide whether to delay a request:
function estimateTokens(text) {
return Math.ceil(text.length / 4);
}
async function throttledCall(payload, tokenBudget) {
const estimated = estimateTokens(JSON.stringify(payload.messages));
if (estimated > tokenBudget.remaining) {
await new Promise((r) => setTimeout(r, tokenBudget.windowMs));
tokenBudget.remaining = tokenBudget.max;
}
tokenBudget.remaining -= estimated;
return callClaude(payload);
}
For precise, production-grade tracking, read the anthropic-ratelimit-tokens-remaining header from each response and update your local budget from the real number instead of your estimate.
Queue, don't drop
User-facing requests should never just fail on a 429 — queue them and process in order. A lightweight in-memory queue works for single-instance apps; for anything running multiple processes or serverless functions, back it with Redis (via BullMQ or similar) so rate limits are respected across your whole fleet, not just one process.
import { Queue, Worker } from 'bullmq';
const claudeQueue = new Queue('claude-requests');
new Worker(
'claude-requests',
async (job) => callClaude(job.data.payload),
{ concurrency: 5 }
);
This also makes retries and backoff centralized instead of scattered across every call site.
Monitor before you hit the wall
Log the rate-limit headers on every response, not just when you get a 429. Tracking requests-remaining and tokens-remaining over time tells you when you're approaching a ceiling, so you can scale concurrency down proactively or upgrade your tier before users see errors.
If you're managing Claude access across a team, a lot of this bookkeeping gets simpler when you route through an API layer that already handles key management and usage visibility. SubToAPI wraps your Claude access in a standard HTTPS API — see /docs/quickstart — with per-key usage metadata in the dashboard, so you can see consumption per application key without building your own logging pipeline. Streaming and tool-use requests (/docs/streaming, /docs/tools) go through the same rate-limit-aware infrastructure, and /pricing covers Solo, Team, and Scale plans with a free trial at /signup.
Putting it together
A solid Node.js setup combines:
- Concurrency limiting sized to your actual RPM tier
- Token-aware throttling for large-context requests
- Exponential backoff with jitter honoring
retry-after - A persistent queue (Redis-backed for multi-instance apps)
- Header-based monitoring so you catch ceilings before users do
None of these replace the others — RPM limiting without token awareness still fails on large prompts, and backoff without concurrency control just delays the inevitable 429.
questions
Does increasing my Claude account tier remove the need for rate-limit handling? No. Higher tiers raise the ceiling but don't remove it, and traffic spikes can still exceed any limit. Backoff and queuing remain necessary regardless of tier.
Should I rate-limit on the client or the server? Always rate-limit server-side. Client-side throttling (in a browser, for instance) can be bypassed or run in parallel across many users, so the enforcement point needs to be your backend or API layer.
What's the simplest fix if I just want fewer 429s without building this myself? Lower your concurrency limit first — it fixes most cases. If you need usage visibility and built-in key management across a team, routing through a managed layer like SubToAPI (/docs/messages) avoids building the queuing and monitoring stack yourself.