Claude API Streaming Timeout Handling Guide
Streaming responses from the Claude API keep a connection open for as long as the model is generating tokens, which can be anywhere from a second to over a minute for long completions. That open connection is exactly what breaks: proxies, load balancers, corporate firewalls, and even some HTTP client libraries assume a request either finishes quickly or is dead, and they kill it. The result is a stream that stops mid-response with no error from the model itself — just a closed socket.
Handling this correctly means separating two distinct problems: total request timeouts (your client gives up waiting for the whole response) and idle timeouts (nothing has been received for N seconds, even though the connection is technically open). Most "Claude API timed out" bug reports are actually one of these two misconfigured on the client or infra side, not a problem with the model or the API itself. Below is a practical breakdown of how to detect, configure, and recover from each.
Why streaming connections time out
A few common causes, roughly in order of frequency:
- Default HTTP client timeouts. Many HTTP libraries default to 30 or 60 seconds total request timeout, which is too short for long streamed completions with reasoning or large outputs.
- Idle-time cutoffs on proxies. Nginx, load balancers, and API gateways often close connections after a fixed idle period (commonly 60s) regardless of your application-level timeout.
- Serverless function limits. Platforms like AWS Lambda or Vercel functions have hard execution ceilings that will kill a stream outright, independent of any timeout you set in code.
- Client-side abort logic that's too aggressive. Code that resets a single timer at request start (instead of on each received chunk) will fire even during a healthy, actively-streaming response.
- Network instability. Mobile networks and long-lived TCP connections through NAT can silently drop without either side sending a close frame.
Idle timeout vs total timeout
This distinction matters because they need different code.
- Total timeout: caps the entire request lifetime, from first byte sent to last byte received. Use this as a hard ceiling to prevent runaway requests.
- Idle timeout: resets every time a chunk arrives. Use this to detect a stalled connection while a slow-but-healthy stream is still fine.
If you only implement a total timeout, you'll kill legitimate long responses. If you only implement an idle timeout, a connection that never sends any data (but also never errors) can hang indefinitely.
Client-side pattern: reset on each chunk
Here's a Node.js example using AbortController with both an idle timeout and a total timeout:
async function streamWithTimeouts(url, options, { idleMs = 30000, totalMs = 180000 } = {}) {
const controller = new AbortController();
const totalTimer = setTimeout(() => controller.abort('total-timeout'), totalMs);
let idleTimer;
const resetIdle = () => {
clearTimeout(idleTimer);
idleTimer = setTimeout(() => controller.abort('idle-timeout'), idleMs);
};
resetIdle();
const response = await fetch(url, { ...options, signal: controller.signal });
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
resetIdle(); // chunk received, connection is alive
process.stdout.write(decoder.decode(value, { stream: true }));
}
} catch (err) {
if (controller.signal.aborted) {
console.error(`Stream aborted: ${controller.signal.reason}`);
}
throw err;
} finally {
clearTimeout(totalTimer);
clearTimeout(idleTimer);
}
}
The key detail: resetIdle() is called every time a chunk arrives, not once at the start. This lets a slow-but-active stream run for minutes while still catching a genuine stall within seconds.
curl: setting the right flags
For quick debugging or shell scripts, curl's default timeouts often cut streams short. Use --max-time for a hard ceiling and be aware that --keepalive-time alone won't help with idle server-side silence — you need -N (no buffering) so you actually see data as it arrives:
curl -N --max-time 180 \
-H "content-type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"claude-opus-4-5","max_tokens":1024,"stream":true,"messages":[{"role":"user","content":"Write a long explanation"}]}' \
https://api.anthropic.com/v1/messages
Without -N, curl buffers output and can look like it's hanging even when data is flowing.
Retry and reconnect strategy
Streaming APIs don't support resuming a partial stream — if the connection drops, you have to restart the request. A few rules that make this safe:
- Only retry on the specific failure types you expect: idle timeout, connection reset, 5xx from the gateway. Don't retry on 4xx errors from the API itself.
- Use exponential backoff with jitter (e.g., 500ms, 1s, 2s, capped at 8s) to avoid hammering the endpoint during a transient outage.
- Cap retries at 2-3 attempts. If a stream keeps failing after that, surface the error to the user rather than looping silently.
- Discard partial output on retry unless your application logic can safely dedupe or merge partial completions — most can't, since the model may generate different text on a retry.
async function withRetry(fn, attempts = 3, baseDelay = 500) {
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (err) {
if (i === attempts - 1) throw err;
const delay = baseDelay * 2 ** i + Math.random() * 200;
await new Promise((r) => setTimeout(r, delay));
}
}
}
Infra-level checklist
If you're running requests through your own proxy or gateway, check these settings before assuming the timeout is a client bug:
- Nginx:
proxy_read_timeoutandproxy_send_timeout(default 60s — raise for long streams) - AWS ALB: idle timeout attribute (default 60s)
- Cloudflare: streaming responses require disabling buffering for that route
- Serverless functions: verify the max execution duration exceeds your expected stream length, or move streaming endpoints to a long-running service instead
If you're using SubToAPI to turn your existing Claude access into an HTTPS API, streaming is handled the same way as the native Claude API — chunked SSE responses over a standard connection — so the same idle/total timeout pattern above applies directly. Full details are in the streaming docs, and the quickstart has a working example if you're setting this up for the first time.
questions
How long can a Claude API stream stay open before timing out? There's no fixed server-side limit for a healthy, actively-generating stream — it can run for minutes on long completions. The practical ceiling is whatever your client, proxy, or hosting platform imposes, so check those layers first.
Should I use idle timeout or total timeout for streaming requests? Use both. Idle timeout (reset on every chunk) catches genuine stalls quickly without killing slow-but-healthy streams; total timeout is a hard ceiling that prevents a request from running forever.
Can I resume a Claude API stream after it times out? No — streaming responses can't be resumed mid-generation. On timeout, discard the partial output and retry the full request with backoff, capping retries at 2-3 attempts before surfacing an error.