zeroclaw-labs/zeroclaw · error · anyhow::Error
MCP server `{server_name}` timed out after {timeout_secs}s b
Error message
MCP server `{server_name}` timed out after {timeout_secs}s before writing {operation} What it means
The overall timeout budget expired before the request was ever written — typically while waiting behind the serial gate, a pending recovery, or the epoch write gate. Because nothing was sent, this is the safe flavor of timeout: the operation definitively did not happen, so retrying cannot duplicate it.
Source
Thrown at crates/zeroclaw-tools/src/mcp_client.rs:666
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Timeout)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(::serde_json::json!({
"mcp_server": &server_name,
"rpc_method": rpc_method,
"timeout_secs": timeout_secs,
"outcome_unknown": unknown_epoch.is_some(),
})),
"mcp_client: MCP request timed out"
);
if let Some(epoch) = unknown_epoch {
self.spawn_recovery(epoch, operation.to_string());
bail!(
"MCP server `{server_name}` timed out after {timeout_secs}s during \
{operation}; outcome unknown and request was not replayed"
);
}
bail!(
"MCP server `{server_name}` timed out after {timeout_secs}s before writing \
{operation}"
);
}
Ok(Ok(response)) => {
cancellation_guard.disarm();
return Ok(response);
}
Ok(Err(error)) => {
if let Some(epoch) = lifecycle.outcome_unknown_epoch() {
cancellation_guard.disarm();
self.spawn_recovery(epoch, operation.to_string());
return Err(error).with_context(|| {
format!(
"MCP server `{server_name}` failed during {operation}; outcome \
unknown and request was not replayed"
)
});View on GitHub (pinned to 88bb9c8533)
Solutions
- Retry immediately — the request never left the client, so there is no duplicate risk
- Raise timeout_secs if legitimate queuing (recovery, serialization) regularly consumes the budget
- Reduce or batch concurrent calls to the same server
- If recovery is the recurring holdup, fix the underlying connection instability flagged in the logs
Defensive patterns
Strategy: retry
Type guard
fn is_pre_write_timeout(err: &anyhow::Error) -> bool {
err.to_string().contains("before writing") && err.to_string().contains("timed out")
} Try / catch
Safe to retry immediately with the same arguments — the request never left the client. If it recurs, back off and raise the timeout instead of tight-looping.
Prevention
- Size timeout_secs to include queuing behind recovery and serialization, not just the RPC itself
- Batch or throttle concurrent calls to a single MCP server
- Fix recurring recovery stalls so the budget is spent on the request, not the wait
When it happens
Trigger: A prior request's recovery still in progress when the budget ran out; many concurrent calls serialized onto one server; a timeout so small that even lock acquisition exceeds it.
Common situations: Bursty fan-out to a single MCP server; a global timeout tuned for fast tools but applied while the server is stuck in recovery; undersized budgets under load.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- MCP server `{server_name}` exhausted the {timeout_secs}s bud
- 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
- Schema missing required 'type' field
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/1d914b846d0b0630.
Report an issue: GitHub.