aaif-goose/goose · error · anyhow::Error
Ollama stream stalled: no data received for {}s. This may in
Error message
Ollama stream stalled: no data received for {}s. This may indicate the model is overwhelmed by the request payload. Try a smaller model, reduce the number of tools, or increase the timeout via OLLAMA_STREAM_TIMEOUT, GOOSE_STREAM_TIMEOUT, or OLLAMA_TIMEOUT in your config. What it means
goose wraps the Ollama SSE stream with a per-chunk watchdog: after the first SSE line arrives, every subsequent line must arrive within a timeout or the stream aborts with this error. The budget resolves from OLLAMA_STREAM_TIMEOUT, then GOOSE_STREAM_TIMEOUT, then OLLAMA_TIMEOUT, else the 120-second default (OLLAMA_DEFAULT_CHUNK_TIMEOUT_SECS). Time-to-first-token is deliberately exempt; only gaps between chunks count. It fires when the server stops sending mid-generation — typically an overwhelmed local model, OOM/crash of the ollama process, or a network/proxy drop.
Source
Thrown at crates/goose-providers/src/ollama.rs:495
stream: impl futures::Stream<Item = anyhow::Result<String>> + Unpin + Send + 'static,
timeout_secs: u64,
) -> std::pin::Pin<Box<dyn futures::Stream<Item = anyhow::Result<String>> + Send>> {
let timeout = Duration::from_secs(timeout_secs);
Box::pin(try_stream! {
let mut stream = stream;
// Allow time-to-first-token to be governed by the request timeout.
// Only enforce per-chunk timeout after first SSE line arrives.
match stream.next().await {
Some(first_item) => yield first_item?,
None => return,
}
loop {
match tokio::time::timeout(timeout, stream.next()).await {
Ok(Some(item)) => yield item?,
Ok(None) => break,
Err(_) => {
Err::<(), anyhow::Error>(anyhow::anyhow!(
"Ollama stream stalled: no data received for {}s. \
This may indicate the model is overwhelmed by the request payload. \
Try a smaller model, reduce the number of tools, or increase the \
timeout via OLLAMA_STREAM_TIMEOUT, GOOSE_STREAM_TIMEOUT, or \
OLLAMA_TIMEOUT in your config.",
timeout_secs
))?;
}
}
}
})
}
/// Ollama-specific streaming handler with XML tool call fallback.
/// Uses the Ollama format module which buffers text when XML tool calls are detected,
/// preventing duplicate content from being emitted to the UI.
/// Timeout is applied at the raw SSE line level via with_line_timeout so that
/// buffering inside response_to_streaming_message_ollama does not cause false stalls.View on GitHub (pinned to 3810898a74)
Solutions
- Raise the budget: export OLLAMA_STREAM_TIMEOUT=600 (checked first), or GOOSE_STREAM_TIMEOUT / OLLAMA_TIMEOUT; note OLLAMA_TIMEOUT also caps the overall request, so raise it too for long generations
- Shrink the payload: disable unneeded MCP tools/extensions, trim conversation history, lower num_ctx/context limit
- Use a smaller/quantized model or offload less (check 'ollama ps'); for CPU inference, budget minutes per response, not seconds
- Check ollama's own logs (journalctl -u ollama or OLLAMA_DEBUG=1) for OOM kills or model load errors — if the process died, no timeout increase will help
- If a proxy sits in the path, raise its read/idle timeout above OLLAMA_STREAM_TIMEOUT
Example fix
# before export OLLAMA_TIMEOUT=600 # per-chunk gap still capped at 120s default -> stalls abort # after export OLLAMA_STREAM_TIMEOUT=600 # per-chunk watchdog export OLLAMA_TIMEOUT=900 # overall request budget # plus: goose session --no-extensions (or disable unused tools) to shrink the payload
Defensive patterns
Strategy: retry
Validate before calling
fn chunk_timeout_secs() -> u64 {
["OLLAMA_STREAM_TIMEOUT", "GOOSE_STREAM_TIMEOUT", "OLLAMA_TIMEOUT"]
.iter().find_map(|k| std::env::var(k).ok().and_then(|v| v.parse().ok()))
.unwrap_or(120)
}
// Before long jobs: ensure the budget fits the backend, e.g. bump for CPU inference:
// if running_on_cpu() { std::env::set_var("OLLAMA_STREAM_TIMEOUT", "600"); } Try / catch
// Distinguish a stall (retryable, payload-driven) from a dead server (not retryable):
let mut attempt = 0;
loop {
attempt += 1;
match run_stream(&req).await {
Err(e) if e.to_string().contains("stream stalled") && attempt < 3 => {
tracing::warn!("stall on attempt {attempt}; trimming tools and retrying");
req = trim_tools(req, 0.5).await; // shrink payload between attempts
continue;
}
other => break other,
}
}?; Prevention
- Set OLLAMA_STREAM_TIMEOUT to match the slowest realistic inter-chunk gap (minutes for CPU/big models), and raise OLLAMA_TIMEOUT to cover total generation
- Keep enabled MCP tools minimal — tool schemas ride along on every request and slow first tokens/chunks
- Watch ollama health (ollama ps, dmesg for OOM) so you distinguish an overloaded model from a crashed daemon
- Raise proxy idle/read timeouts above the stream timeout when a reverse proxy fronts Ollama
When it happens
Trigger: Streaming a large prompt (dozens of MCP tools, long context) to a slow backend — CPU-only inference, big parameter counts, deep reasoning models pausing between reasoning and output — where one inter-chunk gap exceeds the timeout; or the ollama daemon being OOM-killed / the connection severed mid-stream.
Common situations: Laptops running 70B models on CPU; goose sessions with many enabled MCP extensions so the tool schema bloats every request; reverse proxies (nginx/Cloudflare) with idle read timeouts shorter than the model's think time; ollama loading a second model and swapping VRAM mid-response.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Response body is null
- Download failed after {} retries due to stream interruption
- Ollama provider does not support non-streaming mode. All Oll
- HTTP error! status: ${response.status}
- Failed to fetch OIDC config: ${configResp.status}
AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16).
Data as JSON: /api/errors/e2d2d789cf788270.
Report an issue: GitHub.