How to Handle Claude API Timeouts Gracefully
Claude API timeouts happen when a request takes longer than your client (or a proxy in between) is willing to wait for a response. The fix isn't a single setting — it's a combination of setting realistic timeout values, retrying with backoff on the right error types, streaming responses instead of waiting for the full completion, and designing your application so a slow Claude response never becomes a broken user experience.
This matters more with Claude than with most APIs because generation time scales with output length and model choice. A short classification call might finish in 2 seconds; a long-form generation with a large context window and extended thinking can legitimately take 30–90 seconds. If your timeout logic doesn't account for that, you'll see false timeouts on requests that were actually going to succeed, and you'll retry work that duplicates cost and effort.
Why Claude API calls time out
There are three distinct failure modes, and they need different handling:
- Client-side timeout: your HTTP client (fetch, axios, requests) gives up before the server responds. This is the most common cause and the easiest to fix.
- Network/proxy timeout: a load balancer, API gateway, or serverless function (looking at you, Lambda's default limits) cuts the connection regardless of what your code's timeout says.
- Server-side slowness: the model is genuinely taking a long time to generate a long response, especially with large max_tokens, big contexts, or extended thinking enabled.
Before adding retry logic, confirm which one you're actually hitting. Log the elapsed time on every failed call — if failures cluster right at your configured timeout value, it's client-side. If they cluster at a fixed number like 29 or 30 seconds regardless of your code, suspect a gateway or serverless timeout.
Set timeouts based on expected output length, not a fixed number
A flat 10-second timeout works for short responses and fails constantly for long ones. Instead, scale your timeout to the request:
function timeoutForRequest(maxTokens) {
// baseline + a per-token allowance, with a sane floor and ceiling
const base = 10_000;
const perToken = 15; // ms per output token, adjust from your own logs
return Math.min(Math.max(base + maxTokens * perToken, 15_000), 120_000);
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutForRequest(maxTokens));
try {
const res = await fetch(endpoint, { signal: controller.signal, ...options });
} finally {
clearTimeout(timeout);
}
Tune the per-token constant against your own traffic instead of guessing — it varies by model and by how much the prompt relies on tool use or thinking.
Use streaming to avoid timing out on long responses
The single biggest win for perceived timeouts is switching from a blocking request to a streaming one. With streaming, you get the first tokens back in a second or two and keep receiving data as it generates, so there's no single long wait for a client or proxy to time out on. It also lets you show progress to the user instead of a spinner.
If you're building against SubToAPI, streaming works the same way as a direct Claude integration — set "stream": true and read server-sent events as they arrive. See /docs/streaming for the full event format and a working client example.
const res = await 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: 2048,
stream: true,
messages: [{ role: "user", content: "Summarize this report..." }],
}),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let text = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
text += decoder.decode(value);
// parse SSE chunks here and append to your UI incrementally
}
Streaming doesn't eliminate timeouts entirely — a connection can still drop mid-stream — but it turns a single catastrophic failure into a partial result you can often salvage or resume from.
Retry with backoff, but only on the right errors
Blind retries on every failure waste money and can make rate-limiting worse. Retry only on timeouts, 429s, and 5xx responses — never on 400-level errors like bad requests or invalid API keys, since those will fail identically every time.
async function callWithRetry(fn, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
const retryable = err.name === "AbortError" || err.status === 429 || err.status >= 500;
if (!retryable || attempt === maxAttempts) throw err;
const delay = Math.min(1000 * 2 ** attempt, 15_000) + Math.random() * 500;
await new Promise((r) => setTimeout(r, delay));
}
}
}
Exponential backoff with jitter prevents a thundering herd of retries hitting the API at the exact same moment after a transient outage. Cap total attempts at 2–3 — beyond that you're usually masking a real problem rather than recovering from a blip.
Design for graceful degradation, not just retries
Timeouts will happen occasionally no matter how well you tune things, so the application layer needs a fallback path:
- Return a clear "still processing" state to the user instead of a generic error, especially for long-running generations.
- Persist partial streamed output so a dropped connection doesn't lose everything generated so far.
- For async workflows, decouple the request from the response — kick off the call, return a job ID immediately, and let the client poll or receive a webhook when it's done.
- Set
max_tokensconservatively for latency-sensitive endpoints; a smaller cap means a faster, more predictable response even if it costs you a follow-up request for longer content.
If you're running Claude behind SubToAPI, the dashboard shows per-request latency and token usage alongside each call, which makes it much faster to spot whether slow responses correlate with specific models, prompt sizes, or times of day. Check /docs/messages for request/response shape details and /docs/quickstart to get a key set up if you haven't already.
Questions
What's a reasonable default timeout for Claude API calls? For short, non-streaming completions, 30 seconds is a safe starting point. For longer generations or anything using extended thinking, scale the timeout with expected output length rather than using one fixed value everywhere.
Should I retry every failed Claude API request? No. Retry on timeouts, 429 rate limits, and 5xx server errors with exponential backoff. Don't retry 400-level errors like invalid requests or authentication failures — they'll fail the same way every time.
Does streaming prevent timeouts entirely? No, but it drastically reduces their impact. Streaming returns the first tokens quickly and keeps the connection active with continuous data, so proxies and clients are far less likely to cut it off mid-request compared to waiting for one large blocking response.