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

  1. Retry immediately — the request never left the client, so there is no duplicate risk
  2. Raise timeout_secs if legitimate queuing (recovery, serialization) regularly consumes the budget
  3. Reduce or batch concurrent calls to the same server
  4. 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

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

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/1d914b846d0b0630. Report an issue: GitHub.