← Blog

Claude API Retry Logic Best Practices

2026-10-18 · 6 min read · SubToAPI Team

Retrying failed Claude API calls correctly means distinguishing between errors you should retry (rate limits, timeouts, 5xx server errors) and errors you shouldn't (bad requests, auth failures, content policy rejections), then backing off exponentially with jitter so you don't hammer the API during an outage or quota reset window.

Get this wrong in either direction and you pay for it: retry everything blindly and you'll burn through rate limits during an outage, amplify costs on malformed requests that will never succeed, or duplicate side effects like sending an email twice. Retry nothing and a transient network blip or a momentary 429 kills a user-facing feature that would have worked fine one second later. Below is a practical, production-tested approach to retry logic for Claude, plus the specific error codes and headers you need to handle.

Which errors are retryable

Not every failure deserves a retry. Classify errors first:

Retry these:

Don't retry these:

Retrying a 400 or 401 wastes time and quota — the request will fail identically every time until you fix the underlying issue. A useful mental model: retry failures that are about capacity or timing, not failures that are about correctness.

function isRetryable(error) {
  if (!error.status) return true; // network error, no response at all
  return [429, 500, 502, 503, 504].includes(error.status);
}

Exponential backoff with jitter

A fixed delay between retries causes thundering-herd problems — every client backs off for exactly the same duration and then retries simultaneously, recreating the spike that caused the rate limit in the first place. Exponential backoff with jitter spreads retries out over time.

async function callWithRetry(fn, maxRetries = 5) {
  let attempt = 0;
  while (true) {
    try {
      return await fn();
    } catch (err) {
      if (!isRetryable(err) || attempt >= maxRetries) throw err;

      const baseDelay = Math.min(1000 * 2 ** attempt, 30000);
      const jitter = Math.random() * baseDelay * 0.5;
      const delay = baseDelay + jitter;

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

Starting at 1 second and doubling up to a 30-second cap keeps retries fast for brief blips without turning a sustained outage into an infinite hammering loop. Five attempts is a reasonable default for most backend workloads — fewer for latency-sensitive user-facing requests, more for background batch jobs.

Respect the Retry-After header

When you get a 429, don't guess the delay — check for a Retry-After header and use it if present. The API is telling you exactly how long to wait before your quota resets.

function getRetryDelay(err, attempt) {
  const retryAfter = err.headers?.["retry-after"];
  if (retryAfter) return Number(retryAfter) * 1000;

  return Math.min(1000 * 2 ** attempt, 30000);
}

This is more precise than blind exponential backoff and reduces wasted retry attempts, since you're waiting exactly as long as needed instead of guessing.

Timeouts need their own strategy

A request that hangs is different from a request that fails fast. Set an explicit timeout on every call — don't rely on default HTTP client behavior, which can leave connections open for minutes. For streaming responses, also track time-since-last-chunk, not just total request time, since a stream can start fine and then stall.

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);

try {
  const response = await fetch(url, {
    signal: controller.signal,
    // ...
  });
} finally {
  clearTimeout(timeout);
}

If a request times out, treat it like a retryable network error — but be aware that for non-idempotent operations, a timeout doesn't necessarily mean the request failed server-side. It might have succeeded and the response just didn't arrive in time.

Idempotency and duplicate side effects

If your Claude calls trigger tool use that writes to a database, sends a notification, or charges a card, retries can cause duplicate side effects. The fix is to make the side effect, not the API call, idempotent:

This matters more than people expect once you add tool use or agentic loops into the mix — a single logical "turn" might involve multiple underlying API calls, and a retry at any point in that chain can replay earlier side effects if you're not careful. See /docs/tools for details on how tool use requests are structured if you're building this on top of SubToAPI.

Circuit breakers for sustained outages

Exponential backoff handles brief blips well but is wasteful during a genuine multi-minute outage — you'll keep retrying every request for 30 seconds each when you could fail fast instead. A simple circuit breaker helps:

This protects your own application's latency during provider-side incidents instead of queuing up a backlog of slow, doomed requests.

Logging and observability

Log every retry attempt with the error type, status code, attempt number, and delay — not just the final outcome. This is what lets you tell the difference between "occasional transient blips, working as intended" and "something is systematically wrong," before it becomes a support ticket. If you're tracking usage and errors across a team, a dashboard that surfaces retry rates per API key is far more useful than digging through raw logs after the fact — this is one of the things SubToAPI's usage metadata is designed to surface automatically, alongside request latency and token counts, without you having to build that instrumentation yourself.

Putting it together

A solid retry setup combines: classify the error, check Retry-After first, fall back to exponential backoff with jitter, cap total attempts and total elapsed time, set explicit timeouts, keep side effects idempotent, and add a circuit breaker if you're operating at scale. None of this is Claude-specific — it's standard distributed-systems hygiene — but Claude's specific status codes and headers determine exactly how to implement it. If you want retry behavior, rate-limit headers, and usage tracking handled for you rather than hand-rolled, /docs/quickstart walks through getting an API key and making your first request against a layer that already applies these patterns.

questions

Should I retry a timeout on a non-idempotent request? Be cautious. A timeout means you didn't get a response in time, not that the request failed. For non-idempotent operations (charges, sends), use a request ID and check server-side state before retrying, rather than blindly resubmitting.

How many retries is too many? For user-facing requests, 3–5 attempts with a capped total wait time (e.g., under 60 seconds) is typical. Background jobs can tolerate more retries and longer backoff windows since latency matters less.

Does SubToAPI handle retry logic for me? SubToAPI exposes standard HTTP status codes and rate-limit headers so your existing retry logic works unmodified, and surfaces per-key usage and error data in the dashboard — see /docs and /pricing for details on what's included at each plan tier.

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 →