← Blog

Claude API Streaming Timeout Handling Guide

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

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:

Idle timeout vs total timeout

This distinction matters because they need different code.

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:

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:

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.

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 →