← Blog

Claude API Retry with Exponential Backoff

2026-10-12 · 5 min read · SubToAPI Team

Exponential backoff is a retry strategy where you wait progressively longer between failed request attempts, instead of retrying immediately or at a fixed interval. For the Claude API, you need this because transient failures — rate limits (429), server overload (529), and occasional 500/503 responses — happen even with correct code, and hammering the API immediately after a failure usually makes things worse, not better.

The core idea: on the first retry, wait roughly 1 second. On the second, wait 2 seconds. On the third, 4 seconds, then 8, then 16, up to a sensible cap. You add small random jitter to each wait so that if many requests fail at once, they don't all retry at exactly the same moment and cause a new spike. Below is a working implementation, what errors you should and shouldn't retry, and how this changes if you're calling Claude through an API gateway like SubToAPI instead of directly.

Which Claude API errors are retryable

Not every failure should trigger a retry. Retrying the wrong error wastes time and can mask a real bug.

Retry these:

Don't retry these:

If you retry a 400 or 401 in a loop, you'll just burn through your retry budget hitting the same error five times before giving up. Check the status code first and branch accordingly.

A basic exponential backoff implementation

Here's a JavaScript implementation you can drop into any project that calls Claude's Messages API directly:

async function callClaudeWithRetry(payload, {
  maxRetries = 5,
  baseDelayMs = 1000,
  maxDelayMs = 32000,
} = {}) {
  const retryableStatus = new Set([429, 500, 503, 529]);

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = 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 (response.ok) return await response.json();

      if (!retryableStatus.has(response.status) || attempt === maxRetries) {
        const body = await response.text();
        throw new Error(`Claude API error ${response.status}: ${body}`);
      }

      // fall through to backoff below
    } catch (err) {
      if (attempt === maxRetries) throw err;
      // network error — also retried
    }

    const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
    const jitter = Math.random() * exponential * 0.5;
    const delay = exponential * 0.5 + jitter;

    await new Promise((resolve) => setTimeout(resolve, delay));
  }
}

This gives you a wait sequence that roughly doubles each time (with ±25% jitter applied), capped at 32 seconds, and stops after 5 attempts. Adjust maxRetries and maxDelayMs based on how latency-sensitive your caller is — a user-facing chat UI should probably cap total retry time at 10-15 seconds, while a background batch job can afford to wait minutes.

Respecting the Retry-After header

If Claude's API returns a retry-after header on a 429 response, use it instead of guessing:

const retryAfter = response.headers.get("retry-after");
const delay = retryAfter
  ? parseInt(retryAfter, 10) * 1000
  : exponentialDelayWithJitter;

This is more accurate than blind exponential backoff when the server explicitly tells you how long to wait.

Python version for reference

import random
import time
import requests

RETRYABLE_STATUS = {429, 500, 503, 529}

def call_claude_with_retry(payload, max_retries=5, base_delay=1.0, max_delay=32.0):
    for attempt in range(max_retries + 1):
        resp = requests.post(
            "https://api.anthropic.com/v1/messages",
            headers={
                "x-api-key": ANTHROPIC_API_KEY,
                "anthropic-version": "2023-06-01",
            },
            json=payload,
        )

        if resp.ok:
            return resp.json()

        if resp.status_code not in RETRYABLE_STATUS or attempt == max_retries:
            resp.raise_for_status()

        delay = min(max_delay, base_delay * 2 ** attempt)
        delay = delay / 2 + random.uniform(0, delay / 2)
        time.sleep(delay)

Where retries fit in a larger error-handling strategy

Exponential backoff solves one specific problem: transient failures. It doesn't help with structural issues like hitting your organization's rate limit tier permanently, or sending malformed requests. For those, you need different fixes — upgrading your rate limit tier, fixing your payload validation, or adding a circuit breaker so you stop calling an endpoint entirely after repeated hard failures rather than retrying forever.

If you're building a product on top of Claude rather than a single script, consider whether you want to own this retry logic across every service that calls the API, or centralize it. This is one of the practical reasons teams put SubToAPI in front of their Claude usage: it issues standard sub_live_... API keys per application, so you get one place to configure timeouts and retry behavior rather than reimplementing backoff in every microservice. A request through SubToAPI looks like this:

curl https://api.subtoapi.app/v1/messages \
  -H "Authorization: Bearer $SUBTOAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4-5",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Summarize this report."}]
  }'

The retry logic above works the same way against this endpoint — same status codes, same backoff math — you're just pointing it at a different base URL and auth header. See the quickstart and messages docs for request formats, and pricing if you're evaluating whether to route traffic through a managed layer versus calling Anthropic directly.

Testing your backoff logic

Don't wait for a real 429 to find out if your retry code works. Mock the fetch/request call to return a 429 or 529 on the first N attempts, then a 200, and assert that:

This catches off-by-one errors in retry counts and accidental infinite loops before they hit production.

questions

How many retries should I configure for the Claude API? 3-5 retries is typical. More than that rarely helps — if the API is still failing after 5 exponentially-spaced attempts (roughly 30-60 seconds total), the issue is likely not transient and you should surface the error.

Should I retry streaming requests the same way as non-streaming ones? Retry the initial connection the same way, but if a stream fails partway through after tokens have already arrived, decide whether to resume from scratch or discard partial output — Claude's API doesn't support resuming a stream mid-response, so most implementations restart the full request. See streaming for details on how stream events are structured.

Does exponential backoff guarantee my request eventually succeeds? No. It increases the odds of success during brief rate-limit or overload windows, but if you're consistently exceeding your rate limit tier, backoff just delays the inevitable failure — you need a higher limit or lower request volume, not more retries.

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 →