← Blog

How to Add Retry Logic to the Claude API

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

If you're calling the Claude API in production, you will eventually hit a 429 rate limit, a 529 overloaded error, or a dropped connection mid-stream. The fix isn't to catch the exception and give up — it's to add retry logic that distinguishes between errors worth retrying and errors that mean your request is broken. This guide shows exactly how to implement that in Node.js, with the backoff math and status-code logic you need.

The short answer: retry on 429, 500, 502, 503, and 529 using exponential backoff with jitter, cap retries at 3–5 attempts, respect any retry-after header, and never retry on 400, 401, or 403 since those indicate a request or auth problem that a retry won't fix.

Which Claude API errors are retryable

Not every error should trigger a retry. Here's how to classify them:

Retry these:

Don't retry these:

Retrying a 400 or 401 in a loop just burns time and, if you're not careful, API quota. Fail fast on those and surface the error to your logs immediately.

Exponential backoff with jitter

A naive retry loop that fires immediately on failure makes rate limiting worse — every client retries at the same instant and you get a thundering herd. Exponential backoff spaces out retries, and jitter randomizes the spacing so concurrent clients don't collide.

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

  while (true) {
    try {
      return await fn();
    } catch (err) {
      const status = err.status ?? err.response?.status;
      const retryable = [429, 500, 502, 503, 529].includes(status) || !status;

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

      const retryAfter = err.response?.headers?.get('retry-after');
      const delay = retryAfter
        ? Number(retryAfter) * 1000
        : baseDelayMs * 2 ** attempt + Math.random() * 250;

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

Usage:

const response = await callWithRetry(() =>
  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',
      max_tokens: 1024,
      messages: [{ role: 'user', content: 'Summarize this ticket.' }],
    }),
  }).then((res) => {
    if (!res.ok) {
      const error = new Error(`HTTP ${res.status}`);
      error.status = res.status;
      error.response = res;
      throw error;
    }
    return res.json();
  })
);

This pattern works whether you're calling Claude directly or through a gateway like SubToAPI — the retry logic lives entirely in your client, independent of which endpoint is behind it. If you're on SubToAPI, check your plan's rate limit details and response headers in the docs before tuning maxRetries.

Respecting retry-after headers

When a 429 includes a retry-after header, use it instead of your own backoff calculation — the server is telling you exactly how long to wait. Ignoring it and retrying on your own schedule just extends the time until you succeed. The code above already checks for this header first and falls back to exponential backoff only when it's absent.

Handling retries during streaming

Retrying a non-streaming request is straightforward — you resend the same payload. Streaming is trickier because a connection can drop mid-response, after you've already received partial tokens.

The safe approach is to buffer tokens as they arrive and track whether you received a terminal event (message_stop). If the stream drops before that event, treat the entire response as incomplete and retry the full request from scratch rather than trying to resume from a partial position — Claude's API doesn't support resuming a stream mid-sequence.

async function streamWithRetry(requestFn, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    let completed = false;
    try {
      const chunks = [];
      for await (const event of requestFn()) {
        if (event.type === 'message_stop') completed = true;
        chunks.push(event);
      }
      if (completed) return chunks;
      throw new Error('Stream ended without message_stop');
    } catch (err) {
      if (attempt === maxRetries) throw err;
      await new Promise((r) => setTimeout(r, 500 * 2 ** attempt));
    }
  }
}

See /docs/streaming for the exact event sequence to check for completion.

Idempotency for safety

If your Claude calls trigger side effects — writing to a database, sending an email, charging a customer — a retry after a timeout can be dangerous: the first request might have actually succeeded even though you never got the response. Generate an idempotency key per logical request (not per retry attempt) and store it alongside the result, so a duplicate send can be detected and skipped at your application layer. The Claude API itself doesn't do this for you — it has to be built into your retry wrapper.

Where retry logic belongs in your stack

Put retry logic in one place — a shared HTTP client or SDK wrapper — rather than scattering try/catch blocks across every call site. This makes it trivial to tune backoff parameters, add logging on retry, and swap in metrics (retry count, time-to-success) without touching business logic. If you're using SubToAPI as your gateway, you get a single sub_live_... API key and consistent error codes across models, so your retry wrapper only needs one implementation instead of one per provider. Start with the quickstart and plug the retry function above directly into your existing fetch calls.

FAQs

Should I retry on every error from the Claude API? No. Only retry on 429, 500, 502, 503, 529, and network-level failures. 400, 401, and 403 indicate a problem with the request itself that a retry won't fix — log and surface those immediately.

How many retries is reasonable? 3–5 attempts with exponential backoff is standard. Beyond that, you're likely masking a persistent outage or a misconfigured request, and the delay cost outweighs the benefit.

Does retrying a streaming request resume from where it stopped? No. Claude's streaming API doesn't support resuming mid-stream. If a connection drops before the message_stop event, discard the partial output and retry the full request.

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 →