How to Handle Claude API Downtime Gracefully
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:
- Chat UI: show a cached "we're experiencing delays, please retry" message and queue the user's message for background retry rather than losing it.
- Background jobs (summarization, classification, enrichment): push the job back onto a retry queue with a delay instead of failing it permanently.
- Critical-path decisions: fall back to a simpler rule-based or cached response rather than blocking the whole flow on the model being available.
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:
- error rate (by status code)
- p50/p95/p99 latency
- timeout rate specifically (separate from 5xx)
- retry count and circuit breaker state changes
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.