← Blog

Claude API Retry & Exponential Backoff Pattern Guide

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

When the Claude API returns a 429 rate limit error or a transient 500/529 overload error, the correct response is not to fail immediately or to hammer the endpoint again right away. The standard pattern is exponential backoff with jitter: wait a short, randomized amount of time before retrying, and double that wait on each subsequent failure, up to a capped maximum.

This article walks through exactly how to implement that pattern for Claude API calls, which status codes warrant a retry, which don't, and how to avoid common mistakes like retrying non-idempotent requests or retrying forever on a bad API key.

Why you need retry logic at all

The Claude API, like any hosted LLM endpoint, has rate limits (requests per minute, tokens per minute) and occasional capacity constraints during high load. Even a well-architected client will eventually see:

None of these mean your request was malformed. They mean "try again, but not immediately." Retrying instantly makes the problem worse — you add load to an already-stressed system and often get rate-limited harder. Exponential backoff solves this by spacing out retries so the system has time to recover.

The core algorithm

The pattern has three parts:

  1. Base delay — the initial wait time (commonly 500ms–1s)
  2. Exponential growth — multiply the delay by a factor (usually 2x) on each retry
  3. Jitter — add randomness so multiple clients don't retry in synchronized waves

A typical formula:

delay = min(maxDelay, baseDelay * (2 ^ attempt)) + random(0, jitterMs)

Capping the delay (maxDelay) matters — without it, a client that's retried 8 times could end up waiting minutes, which is usually worse than just failing fast and alerting.

Implementation in JavaScript

Here's a reusable retry wrapper for any fetch-based Claude API call:

async function withBackoff(fn, {
  maxRetries = 5,
  baseDelayMs = 500,
  maxDelayMs = 20000,
} = {}) {
  let attempt = 0;

  while (true) {
    try {
      const response = await fn();

      if (response.status === 429 || response.status === 529 || response.status >= 500) {
        throw { retryable: true, status: response.status };
      }

      return response;
    } catch (err) {
      attempt++;
      const retryable = err.retryable || err.name === 'TypeError'; // network error

      if (!retryable || attempt > maxRetries) {
        throw err;
      }

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

      console.warn(`Retry ${attempt}/${maxRetries} after ${Math.round(delay)}ms (status ${err.status})`);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}

Usage against a Claude-compatible endpoint:

const result = await withBackoff(() =>
  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.' }],
    }),
  })
);

Which status codes should trigger a retry

Not every error is retryable. Retrying a 400 Bad Request just gets you the same error five times in a row with extra latency.

Retry these:

Do not retry these:

A common mistake is wrapping the entire request lifecycle in a generic "retry on any exception" block. That silently retries on bugs in your own request-building code, which just burns tokens and delays the real error from surfacing.

Respect the Retry-After header

When present, the 429 and 529 responses often include a Retry-After header indicating how long to wait before the next attempt. When it's available, prefer it over your own computed exponential delay — it reflects the server's actual state:

const retryAfter = response.headers.get('retry-after');
const delay = retryAfter ? Number(retryAfter) * 1000 : computedExpBackoff;

Backoff for streaming requests

Backoff is straightforward for non-streaming requests because you retry the whole call. For streaming, you need to decide: if the stream drops mid-response, do you retry from scratch or resume? Claude's streaming API does not support resuming a partial stream, so the safe approach is to retry the entire request and discard any partial output you received. Track whether you've already appended partial content to a UI buffer, and clear it before retrying. See /docs/streaming for details on the event format you'll be parsing when you rebuild the retry logic around SSE.

Idempotency and side effects

If your Claude API calls trigger side effects via tool use — writing to a database, calling an external API, sending an email — exponential backoff retries can cause duplicate side effects if the first "failed" request actually succeeded server-side but the response was lost in transit. Design tool handlers to be idempotent (use request IDs, upserts instead of inserts) before you wire up aggressive retry logic. See /docs/tools for how tool calls are structured.

Where SubToAPI fits

If you're running Claude access through SubToAPI, the retry pattern above works unchanged — it's a standard HTTPS API using sub_live_... application keys, so any backoff wrapper built for the native Claude API applies directly. The dashboard also surfaces per-key usage and error rates, which is useful for tuning your maxRetries and baseDelayMs based on real observed rate-limit frequency rather than guessing. Check /docs/quickstart to get a key, or /pricing if you're evaluating plans for a team.

Questions

How many retries should I configure for Claude API calls? 3–5 is typical for interactive user-facing requests. Background jobs can afford more (8–10) since latency matters less there.

Should I use the same backoff strategy for rate limits and server errors? You can use the same exponential curve, but prefer the Retry-After header when present for 429/529 — it's more accurate than a guessed delay.

Does exponential backoff work the same way for streaming requests? Not quite — streams can't resume mid-way, so a dropped stream means retrying the full request and discarding any partial output already rendered.

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 →