zeroclaw-labs/zeroclaw · error · anyhow::Error
MCP server `{server_name}` exhausted the {timeout_secs}s bud
Error message
MCP server `{server_name}` exhausted the {timeout_secs}s budget recovering before writing {operation} What it means
Before this request could be written, a transport error triggered an automatic reconnect; dispatch_rpc waits for that recovery within the same overall deadline, and this error means recovery consumed the entire remaining budget without finishing. The request was never sent — retry is safe — but the connection was still unhealthy at the deadline.
Source
Thrown at crates/zeroclaw-tools/src/mcp_client.rs:704
cancellation_guard.disarm();
let recoverable = error.downcast_ref::<McpTransportError>().is_some();
if recoverable && pre_write_retries < MAX_RECONNECT_ATTEMPTS {
pre_write_retries += 1;
let observed_epoch = lifecycle.pre_write_epoch().unwrap_or(0);
let recovery = self.start_recovery(observed_epoch, operation.to_string());
match timeout_at(deadline, recovery).await {
Ok(Ok(result)) => result?,
Ok(Err(join_error)) => {
return Err(anyhow::Error::new(join_error)).with_context(|| {
format!(
"MCP server `{server_name}` recovery task failed before \
writing {operation}"
)
});
}
Err(_) => {
bail!(
"MCP server `{server_name}` exhausted the {timeout_secs}s \
budget recovering before writing {operation}"
);
}
}
continue;
}
return Err(error).with_context(|| {
format!("MCP server `{server_name}` error during {operation}")
});
}
}
}
}
/// Call a tool on this server. Returns the raw JSON result.
pub async fn call_tool(
&self,View on GitHub (pinned to 88bb9c8533)
Solutions
- Retry once the server has finished starting; the request was not written so there is no duplicate risk
- Increase timeout_secs to cover the server's cold-start plus re-handshake time
- Make the server start faster (lazy imports, fewer deps) instead of only raising timeouts
- If it persists, check for crash-looping: each failed start wastes a full budget window
Defensive patterns
Strategy: retry
Type guard
fn is_recovery_budget_exhausted(err: &anyhow::Error) -> bool {
err.to_string().contains("exhausted the") && err.to_string().contains("budget recovering")
} Try / catch
Retry once after giving the server time to finish starting — the request was never written. Persistent failures mean the server cold-start or crash-loop exceeds the budget; fix that before more retries.
Prevention
- Raise timeout_secs to cover server cold-start plus re-handshake for heavy Node/Python servers
- Keep MCP server startup lean (lazy imports) when timeouts are tight
- Detect crash-looping servers and alert rather than burning budget windows on them
When it happens
Trigger: A slow-starting server process (stdio spawn, dependency loading) whose reset + re-handshake exceeds the remaining budget; recovery racing a nearly-expired deadline; very small timeout values paired with cold-starting servers.
Common situations: Heavy Node/Python MCP servers cold-starting slower than the configured timeout; crash-looping servers where every recovery attempt restarts them and burns the full budget.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- MCP server `{server_name}` timed out after {timeout_secs}s b
- MCP server `{server_name}` is unavailable: a prior request's
- MCP server `{server_name}` timed out after {timeout_secs}s d
- MCP server `{server_name}` recovery task failed before writi
- ACP elicitation/create timed out after {timeout:?}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/b1375c3cbf308191.
Report an issue: GitHub.