{"record":{"id":"0559dbcbfdc15857","repo":"Hmbown/CodeWhale","slug":"sse-stream-idle-timeout-after-s-no-data-receiv-0559db","errorCode":null,"errorMessage":"SSE stream idle timeout after {}s — no data received (bytes_received={}, stream_age_ms={}, ms_since_last_chunk={})","messagePattern":"SSE stream idle timeout after (.+?)s — no data received \\(bytes_received=(.+?), stream_age_ms=(.+?), ms_since_last_chunk=(.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/client/responses.rs","lineNumber":244,"sourceCode":"            let mut buffer: Vec<u8> = Vec::new();\n            let mut done = false;\n            let mut content_block_counter: u32 = 0;\n            let stream_start = std::time::Instant::now();\n            let mut last_chunk_at = std::time::Instant::now();\n            let mut bytes_received: usize = 0;\n\n            tokio::pin!(byte_stream);\n\n            while !done {\n                let chunk = match tokio::time::timeout(stream_idle_timeout, byte_stream.next()).await {\n                    Ok(Some(Ok(chunk))) => chunk,\n                    Ok(Some(Err(e))) => {\n                        yield Err(anyhow::anyhow!(\"Stream read error: {e}\"));\n                        return;\n                    }\n                    Ok(None) => break,\n                    Err(_) => {\n                        yield Err(anyhow::anyhow!(super::stream_entry::idle_timeout_message(\n                            stream_idle_timeout,\n                            bytes_received,\n                            stream_start.elapsed(),\n                            last_chunk_at.elapsed(),\n                        )));\n                        return;\n                    }\n                };\n\n                bytes_received += chunk.len();\n                last_chunk_at = std::time::Instant::now();\n                buffer.extend_from_slice(&chunk);\n\n                // Process complete SSE lines.\n                loop {\n                    let line = match super::take_sse_line(&mut buffer) {\n                        Ok(Some(line)) => line,\n                        Ok(None) => break,","sourceCodeStart":226,"sourceCodeEnd":262,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/client/responses.rs#L226-L262","documentation":"A Responses-API SSE stream went quiet for longer than the configured idle timeout (stream_chunk_timeout_secs, default 123s). The streaming loop in crates/tui/src/client/responses.rs:244 wraps every byte_stream.next() poll in tokio::time::timeout; on Elapsed it emits a diagnostic with bytes_received, total stream age, and ms since the last chunk, then aborts the stream.","triggerScenarios":"Headers succeeded but the body never starts (bytes_received=0), or chunks stop arriving mid-response for > stream_chunk_timeout_secs: server-side generation with no SSE keep-alive comments, a proxy/middlebox that buffers text/event-stream, a half-open TCP connection after a network switch, or an overloaded gateway that accepted the connection then queued indefinitely.","commonSituations":"Long tool-heavy generations where the provider thinks silently for minutes; VPN or corporate proxy drops; self-hosted vllm/sglang gateways that are slow to first token; default 123s budget left unchanged on a slow endpoint.","solutions":["Retry the request - the stream is already aborted and nothing partial is kept","Raise the idle budget: add stream_chunk_timeout_secs = 300 under [network] in the config file","Check intermediaries (nginx, Cloudflare, corporate proxies) for buffering or shorter timeouts on event-stream routes and disable buffering for text/event-stream","If bytes_received=0 on every attempt, reproduce with curl -N against the same endpoint to confirm the provider actually streams","Check the provider status page for degraded streaming capacity"],"exampleFix":"# config.toml - before\n[network]\n# idle budget defaults to 123s\n\n# config.toml - after\n[network]\nstream_chunk_timeout_secs = 300\n","handlingStrategy":"retry","validationCode":"// Rust caller: budget the stream before starting\nlet idle = Duration::from_secs(cfg.stream_chunk_timeout_secs()); // >= 123s default\nassert!(idle.as_secs() >= expected_time_to_first_token(&model), \"idle budget below TTFT estimate\");","typeGuard":null,"tryCatchPattern":"// Match on the message prefix; only retry when nothing was consumed or the call is idempotent\nlet mut attempt = 0;\nloop {\n    match run_streaming_call().await {\n        Ok(v) => break v,\n        Err(e) if e.to_string().starts_with(\"SSE stream idle timeout\") && attempt < 2 => {\n            attempt += 1;\n            tokio::time::sleep(Duration::from_secs(2u64.pow(attempt))).await;\n        }\n        Err(e) => return Err(e),\n    }\n}","preventionTips":["Size stream_chunk_timeout_secs above the provider's worst-case time-to-first-token","Disable proxy buffering and shorten intermediate timeouts on text/event-stream routes","Watch bytes_received in the message: 0 means connect-level stall, >0 means mid-stream stall"],"tags":["network","sse","streaming","timeout","responses-api"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}