CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/API & SDK
API & SDK/2026-05-01Intermediate

When Your Claude API Retry Logic Made Rate Limits Worse — The Retry-After Header You Forgot to Read

If 429 errors went up after you added retry logic to your Claude API client, the cause is almost always the same: ignoring the Retry-After header and using exponential backoff without jitter. Here is how to diagnose and fix it.

claude-api81retry7rate-limit7backofftroubleshooting87anthropic12

"I added retry logic, and somehow my 429 rate is higher than before" — this is the question I get most often from teams that just put Claude API into production. I have hit the same wall myself: my first client wrapped requests in a "wait three seconds, retry up to three times" loop, and during peak traffic the rate limit hits actually went up.

It feels counterintuitive. Retries are supposed to be the polite thing to do. The problem is rarely the retry itself — it is that multiple clients waiting the same fixed interval will all hammer the server at the same instant. Anthropic's docs recommend "exponential backoff," but what actually works in practice is "exponential backoff with full jitter, plus respecting the Retry-After header."

This article walks through the retry traps I have repeatedly fallen into while integrating Claude API into my own apps, in the order you should diagnose them. The code samples are minimal Node.js (no SDK), but the same patterns apply to Python or any other language.

Start by inspecting the headers, not by writing code

Before you change a single line of retry code, look at what the server is telling you. When the API returns 429 or 529, the response usually contains:

  • retry-after: minimum seconds to wait before retrying (a number, or an HTTP date)
  • anthropic-ratelimit-requests-remaining: how many requests you have left in the window
  • anthropic-ratelimit-requests-reset: UNIX timestamp when that count resets
  • anthropic-ratelimit-tokens-remaining: how many tokens you have left
  • anthropic-ratelimit-tokens-reset: UNIX timestamp when the token window resets

The first move is to log every one of these whenever a request fails. I skipped this step at first and walked around with a vague feeling that "things are congested." Once I started recording them, it turned out one specific request was burning through the entire token quota in a single call. The real bug was not retry logic at all — it was a giant uncached prompt being sent in parallel. Some retry "problems" are really design problems hiding behind a noisy log.

// Always capture the relevant headers when a request fails.
async function callClaude(body) {
  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(body),
  });
 
  if (!res.ok) {
    // For 429 / 529, log every header that helps explain the failure.
    console.error({
      status: res.status,
      retryAfter: res.headers.get("retry-after"),
      reqRemaining: res.headers.get("anthropic-ratelimit-requests-remaining"),
      reqReset: res.headers.get("anthropic-ratelimit-requests-reset"),
      tokRemaining: res.headers.get("anthropic-ratelimit-tokens-remaining"),
      tokReset: res.headers.get("anthropic-ratelimit-tokens-reset"),
      requestId: res.headers.get("request-id"),
    });
  }
  return res;
}

Capture the request-id too. The day you have to email Anthropic support, you will be glad you stored it.

Anti-pattern 1: a fixed-delay retry loop

Almost every codebase starts here.

// BAD: same wait every time, no matter what.
async function retryFixed(body, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const res = await callClaude(body);
    if (res.ok) return res;
    if (res.status === 429 || res.status === 529) {
      await new Promise((r) => setTimeout(r, 3000)); // always 3s
      continue;
    }
    return res;
  }
  throw new Error("max retries exceeded");
}

The problem is not the value 3000. The problem is that every client that hit a 429 at the same moment now retries together exactly three seconds later. From the server's point of view, you have just generated a synchronized spike that repeats every three seconds — the classic thundering herd. You meant to be polite; what you actually did was build a small DDoS.

Anti-pattern 2: exponential backoff without jitter

Once people learn about "exponential backoff," they often write this:

// MARGINAL: exponential, but no randomness.
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s...
await new Promise((r) => setTimeout(r, delay));

It is better than a flat delay, but every client still waits the exact same amount of time, so the spikes just become a tidy staircase. The shape of the load on the server is still synchronized; you have only stretched it out.

In my own apps, just adding jitter on top of this dropped the rate of repeat 429s during peak hours by roughly half.

The pattern that actually works: Retry-After first, then full jitter

The "full jitter" backoff popularized by the AWS Architecture Blog works very well for Claude API. The idea is simple: grow the upper bound of the wait exponentially, but pick a random delay inside that bound.

// GOOD: respect Retry-After first, fall back to full-jitter exponential backoff.
function computeBackoffMs(attempt, retryAfter) {
  // 1) If the server tells us how long to wait, honor it.
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (!Number.isNaN(seconds)) return seconds * 1000;
    // HTTP-date format
    const dateMs = Date.parse(retryAfter);
    if (!Number.isNaN(dateMs)) return Math.max(0, dateMs - Date.now());
  }
  // 2) Otherwise: full-jitter exponential backoff.
  const cap = 60_000; // 60s ceiling
  const base = 500;   // 0.5s starting point
  const exp = Math.min(cap, base * 2 ** attempt);
  return Math.floor(Math.random() * exp); // random in [0, exp)
}
 
async function retryWithBackoff(body, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const res = await callClaude(body);
    if (res.ok) return res;
 
    // Only retry 5xx and 429. 4xx (401, 400, etc.) are permanent.
    const retriable = res.status === 429 || res.status === 529 || (res.status >= 500 && res.status < 600);
    if (!retriable) return res;
 
    const wait = computeBackoffMs(attempt, res.headers.get("retry-after"));
    console.warn(`[retry ${attempt + 1}/${maxRetries}] status=${res.status} waiting=${wait}ms`);
    await new Promise((r) => setTimeout(r, wait));
  }
  throw new Error("max retries exceeded");
}

The behavior you want: if the server says "wait three seconds," you wait three seconds. Otherwise the waits become 0–0.5s, 0–1s, 0–2s, 0–4s, and so on, with each client picking a different value inside its window. The synchronized spike vanishes.

Things to check if 429s are still happening after the fix

If you have made the changes above and still see elevated 429 rates, work through this list:

  1. Lack of idempotency: if Claude's response feeds something with side effects (a database write, a payment), retrying creates duplicates. Use message.id for deduplication, or assign your own UUID up front and track an idempotency key.
  2. Concurrency above your tier's limit: each account tier has an implicit cap on simultaneous requests. Even perfect backoff cannot save you if your worker pool is constantly above that cap. Use something like p-limit to cap concurrency explicitly — it is the fastest fix.
  3. Aborting before the response body is read: cutting an HTTP connection mid-response can cause the server to count the request as completed late, wasting your quota. Use AbortController for explicit timeouts, and still drain res.body even on error.
  4. Token quota exhaustion mistaken for request limits: if anthropic-ratelimit-tokens-remaining is hitting zero, throttling requests will not help. The fix is shorter prompts or prompt caching. I cover the diagnosis flow for cache misses in How to diagnose Claude API prompt cache misses.

Streaming responses need a different decision

For streaming endpoints, the retry decision hinges on whether the first message_start event arrived. Restarting a stream from scratch after a mid-stream disconnect double-bills tokens and double-spends your rate quota. I walk through safe handling of mid-stream failures in Production-grade resilience for Claude API streaming; pair it with this article if you are streaming in production.

A pattern I keep around for new projects

Over time I have collected one helper module that I drop into every Claude API project on day one: it wraps fetch, applies the retry pattern above, exposes a concurrency option that is enforced internally, and emits structured logs to the same place as the rest of the request telemetry. It is around eighty lines, but the value is that the first version I deploy already handles 429 sanely — I am not waiting for production traffic to teach me the lesson again. If you maintain more than one Claude integration, building this small abstraction once pays back every time.

One concrete next step

Open your current Claude API client and check just one thing: does it read the retry-after header? If not, drop in the computeBackoffMs snippet above. That single change usually pulls peak-hour error rates down visibly. If concurrency is the real culprit, follow up by wrapping your call site in p-limit. Doing those two in that order has been my reliable recipe.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API & SDK2026-05-29
Diagnosing invalid_request_error When You Pass an Image URL to the Claude API
When the Claude API rejects an image you passed via `source.type: url`, the root cause almost always lives in one of four buckets: scheme, MIME, size, or reachability. Here is the diagnostic order I use in production.
API & SDK2026-04-28
Diagnosing Claude API Prompt Cache Misses — How to Read the usage Field
If your Claude API prompt cache isn't reducing your bill, the usage field is where to start. This guide walks through the five most common reasons cache_read_input_tokens stays at zero and how to fix each one.
API & SDK2026-07-13
Coalescing Concurrent Claude API Calls: Single-Flight Against Duplicate Inference and Cache Stampede
A design for collapsing identical prompts that fire at the same instant into a single upstream Claude call, using single-flight (request coalescing). In-process and distributed implementations, jittered retries, and negative caching, with measured results.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →