← Blog

How to Handle Claude API Downtime Gracefully

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

When Claude API calls start failing — 500s, 503s, connection resets, or requests that just hang — the fix isn't to panic-refresh a status page. It's to build your integration so a single provider outage degrades your app instead of breaking it. That means detecting failure fast, retrying only when it makes sense, failing over or queuing when retries don't help, and telling users what's actually happening.

This article covers the concrete patterns: timeouts, retry/backoff, circuit breakers, fallback responses, and monitoring, plus where a layer like SubToAPI fits if you want some of this handled for you.

Why Claude API downtime hurts more than it should

Most outages aren't total blackouts. They're partial degradations: elevated latency, intermittent 5xx errors, or timeouts on long streaming responses. If your code treats every failure the same way — log it and show a generic error — you turn a 2-minute blip into a bad user experience, or worse, a retry storm that makes the problem worse for everyone.

The goal isn't "never fail." It's "fail small, fail fast, and recover automatically."

1. Set real timeouts, not infinite waits

The most common failure mode isn't an error — it's a request that never comes back. If your HTTP client has no timeout, a single hung connection can tie up a worker thread or Lambda execution indefinitely.

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({
    model: "claude-opus-4",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Summarize this ticket." }],
  }),
  signal: AbortSignal.timeout(15000), // never wait forever
});

Pick a timeout based on your use case: short for interactive chat (10–20s), longer for batch jobs. Streaming requests need separate timeouts for "time to first token" and "time between tokens," since a slow-but-alive stream shouldn't be killed the same way a dead connection should.

2. Retry with exponential backoff — and jitter

Transient errors (429, 500, 502, 503, connection resets) are usually worth retrying. Client errors (400, 401, 403) never are — retrying a malformed request just wastes time.

async function callWithRetry(fn, maxAttempts = 4) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const retryable = [429, 500, 502, 503, 529].includes(err.status);
      if (!retryable || attempt === maxAttempts) throw err;

      const base = Math.min(1000 * 2 ** attempt, 15000);
      const jitter = Math.random() * 500;
      await new Promise((r) => setTimeout(r, base + jitter));
    }
  }
}

Jitter matters: if every client retries at exactly the same backoff interval, you create synchronized retry waves that hit the API at the same moment, which is the exact pattern that prolongs outages. Randomizing the delay spreads retries out.

Cap total retry time to something your user can tolerate — if a chat UI is waiting more than ~30 seconds for a response, retries have stopped helping and it's time to show a fallback.

3. Use a circuit breaker to stop hammering a dead endpoint

Retries help with brief blips. They don't help with a sustained outage — if the API is down for five minutes, retrying every request just multiplies your error rate and burns time.

A circuit breaker tracks failure rate over a rolling window. Once it crosses a threshold, it "opens" and short-circuits calls immediately (returning a fallback) instead of hitting the API at all. After a cooldown, it lets a small number of test requests through to check if the service has recovered.

class CircuitBreaker {
  constructor({ threshold = 5, cooldownMs = 30000 }) {
    this.failures = 0;
    this.threshold = threshold;
    this.cooldownMs = cooldownMs;
    this.state = "closed";
    this.openedAt = 0;
  }

  async call(fn, fallback) {
    if (this.state === "open") {
      if (Date.now() - this.openedAt < this.cooldownMs) return fallback();
      this.state = "half-open";
    }
    try {
      const result = await fn();
      this.failures = 0;
      this.state = "closed";
      return result;
    } catch (err) {
      this.failures++;
      if (this.failures >= this.threshold) {
        this.state = "open";
        this.openedAt = Date.now();
      }
      return fallback();
    }
  }
}

This is the single highest-leverage change you can make for resilience. It bounds the damage of an outage to a short window instead of letting every request slowly time out for as long as the incident lasts.

4. Have a fallback that isn't just an error page

What your fallback does depends on the feature:

The worst fallback is a stack trace in the user's face. The second worst is silently dropping the request with no record that it needs to be retried.

5. Monitor and alert on the right signals

Track, per endpoint and model:

Alert on rate-of-change, not absolute thresholds — a jump from 0.1% to 5% errors matters more than a static "errors > 10" rule, especially at low traffic volumes.

If you're already paying for a layer that normalizes your Claude access into a single HTTPS API, this is a good place to centralize retry/backoff logic and usage metadata instead of reimplementing it per service. SubToAPI exposes application API keys (sub_live_...) with request logging and usage metadata built in, so you can see error spikes across your whole team without instrumenting every client separately — see the quickstart and docs for details. It doesn't eliminate upstream outages, but it gives you one place to watch instead of N.

6. Design for graceful degradation, not just uptime

Resilience isn't a single feature — it's a layered set of defaults: timeouts that fail fast, retries that don't pile on, a circuit breaker that stops the bleeding, and a fallback that keeps the product usable. Build all four, test them by actually killing the connection in staging, and downtime becomes a blip your users barely notice instead of an incident.

FAQ

Should I retry on every error from the Claude API? No. Retry on transient errors like 429, 500, 502, 503, and 529, plus network-level timeouts. Don't retry 400, 401, or 403 — those indicate a request or auth problem that a retry won't fix.

How many retries and how much delay is reasonable? Three to four attempts with exponential backoff (roughly 1s, 2s, 4s, 8s) plus jitter is a sane default for interactive use. Cap total retry time around 20–30 seconds for user-facing requests; background jobs can retry longer with queue-based delays.

What's the difference between a timeout and a circuit breaker? A timeout bounds how long a single request waits before failing. A circuit breaker tracks failures across many requests and stops sending new ones to a failing endpoint for a cooldown period, preventing you from repeatedly hitting a service that's already down.

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 →