← Blog

Claude API Rate Limit Best Practices in Node.js

2026-11-01 · 5 min read · SubToAPI Team

If you're building a production Node.js app on Claude, you will eventually hit a 429 Too Many Requests or overloaded_error response. This happens because Anthropic enforces limits on requests per minute (RPM), tokens per minute (input and output, often tracked separately), and sometimes concurrent requests — all scoped to your API key's tier. The fix isn't to request a higher limit and hope; it's to write client code that respects limits proactively and recovers gracefully when it doesn't.

This article covers the concrete patterns: reading rate limit headers, retrying with exponential backoff and jitter, controlling concurrency so you never burst past your ceiling, and queuing requests so traffic spikes degrade gracefully instead of failing. All examples are plain Node.js — no framework required — and apply whether you're calling Anthropic directly or through a gateway.

Understand what's actually being limited

Claude's API typically enforces three independent limits simultaneously:

A single long-context request can exhaust your ITPM limit well before you hit RPM. This matters for backoff strategy: retrying immediately after a token-limit 429 is pointless if your next request has the same token footprint. Check the response headers before deciding how long to wait.

const res = await fetch("https://api.anthropic.com/v1/messages", options);

console.log(res.headers.get("anthropic-ratelimit-requests-remaining"));
console.log(res.headers.get("anthropic-ratelimit-tokens-remaining"));
console.log(res.headers.get("anthropic-ratelimit-requests-reset"));

Log these in production. They tell you how close you are to a wall before you hit it, which lets you throttle proactively instead of reactively.

Exponential backoff with jitter

A naive retry loop that waits a fixed interval will cause every queued request to retry in lockstep, creating a new burst at the same moment — the classic thundering herd problem. Add jitter to spread retries out.

async function callWithRetry(fn, maxRetries = 5) {
  let attempt = 0;
  while (true) {
    try {
      return await fn();
    } catch (err) {
      const status = err.status || err.response?.status;
      const retryable = status === 429 || status === 529 || status >= 500;
      if (!retryable || attempt >= maxRetries) throw err;

      const base = Math.min(1000 * 2 ** attempt, 30000);
      const jitter = Math.random() * base * 0.5;
      await new Promise((r) => setTimeout(r, base + jitter));
      attempt++;
    }
  }
}

Respect Retry-After if Anthropic sends it — it's more accurate than a guessed backoff curve. Fall back to exponential backoff only when that header is absent.

const retryAfter = res.headers.get("retry-after");
const delayMs = retryAfter ? Number(retryAfter) * 1000 : computedBackoff;

Cap concurrency, don't just retry

Retrying after a 429 treats the symptom, not the cause. If your app fires 50 concurrent Claude calls from a batch job, you'll hit rate limits constantly no matter how good your retry logic is. Use a concurrency limiter so you never exceed a safe number of simultaneous in-flight requests.

import pLimit from "p-limit";

const limit = pLimit(5); // tune based on your tier

const results = await Promise.all(
  items.map((item) => limit(() => callWithRetry(() => callClaude(item))))
);

For high-throughput workloads, pair concurrency limiting with a token-aware queue: estimate the token cost of each request before sending it, and hold back requests once your running total for the current minute approaches your ITPM/OTPM ceiling. A simple sliding-window counter is enough for most apps — you don't need a full scheduling framework unless you're processing thousands of requests per minute.

class TokenWindow {
  constructor(limitPerMinute) {
    this.limit = limitPerMinute;
    this.used = 0;
    this.windowStart = Date.now();
  }

  canSend(estimatedTokens) {
    if (Date.now() - this.windowStart > 60000) {
      this.used = 0;
      this.windowStart = Date.now();
    }
    return this.used + estimatedTokens <= this.limit;
  }

  record(tokens) {
    this.used += tokens;
  }
}

Batch and cache where it makes sense

Rate limits are easier to stay under when you simply send fewer requests. Two practical levers:

Neither of these replaces proper retry and concurrency handling, but they reduce how often you need it.

Monitor usage before you hit the wall

The best rate-limit strategy is the one that never needs a retry because you saw the problem coming. Track RPM and TPM usage per minute in your own logs or metrics system, and alert when you're consistently running above 70–80% of your limit — that's your signal to either optimize token usage or request a tier increase.

If you'd rather not build and maintain this layer yourself — rate-limit headers, backoff, concurrency queues, usage dashboards — that's exactly what SubToAPI wraps around your Claude access. You get an sub_live_... application key, streaming and tool use support, and per-key usage metadata out of the box, so your Node.js code just calls a stable HTTPS endpoint instead of reimplementing rate-limit plumbing. See the quickstart or the Messages API reference for the exact request shape.

const res = await fetch("https://api.subtoapi.app/v1/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "claude-sonnet-4-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Summarize this ticket." }],
  }),
});

Whether you call Anthropic directly or through a gateway, the underlying discipline is the same: cap concurrency, back off with jitter, respect Retry-After, and watch your token budget before you run out of it.

questions

What causes a 429 error on the Claude API? Usually you've exceeded your requests-per-minute, input-tokens-per-minute, or output-tokens-per-minute limit for your API tier. Check the anthropic-ratelimit-* response headers to see which one triggered it.

How long should I wait before retrying a rate-limited request? Use the Retry-After header if present; otherwise use exponential backoff starting around 1 second, doubling up to a cap of 20–30 seconds, with random jitter added to avoid synchronized retries across concurrent requests.

Does limiting concurrency actually prevent rate limit errors? Yes — most rate-limit errors come from bursts of simultaneous requests rather than steady traffic. Capping concurrency with a limiter like p-limit and pairing it with a token-budget check per minute prevents most 429s before they happen.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →