Hmbown/CodeWhale · error

SSE stream idle timeout after {}s — no data received (bytes_

Error message

SSE stream idle timeout after {}s — no data received (bytes_received={}, stream_age_ms={}, ms_since_last_chunk={})

What it means

The Anthropic SSE reader enforces an idle timeout: if no bytes arrive within `stream_idle_timeout` (from `[tui] stream_chunk_timeout_secs`, default 900s, clamped 1-3600), the stream terminates with this diagnostics-rich message showing bytes received so far, total stream age, and time since the last chunk. It fires on the `Err(_)` arm of the `tokio::time::timeout` wrapper — the connection is healthy but silent.

Source

Thrown at crates/tui/src/client/anthropic.rs:325

            // UTF-8 char (CJK/emoji) split across two network reads is never
            // corrupted to U+FFFD. Line boundaries ('\n') are ASCII and can
            // never fall inside a multi-byte sequence. (Mirrors chat.rs.)
            let mut buffer: Vec<u8> = Vec::new();
            let stream_start = std::time::Instant::now();
            let mut last_chunk_at = std::time::Instant::now();
            let mut bytes_received: usize = 0;
            tokio::pin!(byte_stream);

            loop {
                let chunk = match tokio::time::timeout(stream_idle_timeout, byte_stream.next()).await {
                    Ok(Some(Ok(chunk))) => chunk,
                    Ok(Some(Err(e))) => {
                        yield Err(anyhow::anyhow!("Stream read error: {e}"));
                        return;
                    }
                    Ok(None) => break,
                    Err(_) => {
                        yield Err(anyhow::anyhow!(super::stream_entry::idle_timeout_message(
                            stream_idle_timeout,
                            bytes_received,
                            stream_start.elapsed(),
                            last_chunk_at.elapsed(),
                        )));
                        return;
                    }
                };

                bytes_received += chunk.len();
                last_chunk_at = std::time::Instant::now();
                buffer.extend_from_slice(&chunk);

                loop {
                    let line = match super::take_sse_line(&mut buffer) {
                        Ok(Some(line)) => line,
                        Ok(None) => break,
                        Err(err) => {

View on GitHub (pinned to 8880682c63)

Solutions

  1. Raise the idle budget in config: `[tui] stream_chunk_timeout_secs = 1800` (max 3600) if your model legitimately goes silent for long stretches.
  2. Use the diagnostics fields: `bytes_received=0` → the server never started sending (open/first-token problem, check the model and endpoint); `bytes_received>0` → mid-stream stall (network/proxy issue).
  3. For buffering proxies, bypass them for the Anthropic domain or force HTTP/1.1 (`CODEWHALE_FORCE_HTTP1=1`).
  4. Retry the request — the stream is cleanly terminated and the turn can be reissued.

Example fix

# before
[tui]
stream_chunk_timeout_secs = 60    # too tight for long thinking gaps
# after
[tui]
stream_chunk_timeout_secs = 1800
Defensive patterns

Strategy: retry

Validate before calling

// Config sanity check before long sessions: keep the idle budget generous.
fn stream_idle_budget_ok(configured: Option<u64>) -> bool {
    configured.unwrap_or(900) >= 900 // default 900s, clamp range 1..=3600
}

Type guard

fn is_idle_timeout_error(e: &anyhow::Error) -> bool {
    e.to_string().contains("SSE stream idle timeout")
}

Try / catch

match stream.next().await {
    Some(Err(e)) if is_idle_timeout_error(&e) => {
        let m = e.to_string();
        if m.contains("bytes_received=0") {
            // server never started: wrong endpoint/model — do not blind-retry, diagnose
            diagnose_first_token_path().await;
        } else {
            // mid-stream stall: safe to retry the turn once with backoff
            retry_turn_once().await;
        }
    }
    other => handle(other),
}

Prevention

When it happens

Trigger: Extended server-side thinking gaps with no SSE keepalive/ping events; a stalled upstream between the provider and the client; proxies buffering and withholding chunks; streams paused so long the 900s default (or a user-lowered value) expires. Note `bytes_received=0` means nothing ever arrived after headers; a large value with a small `ms_since_last_chunk` gap means the stream ran long then stalled.

Common situations: Deep-reasoning models that go silent for many minutes; users lowering `stream_chunk_timeout_secs` to make failures snappier and then hitting it during legitimate long generations; corporate proxies that hold SSE responses; very long single completions on slow endpoints.

Understand the failure class

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/2b3ef8f5266314c3. Report an issue: GitHub.