Tool calls to a local MCP HTTP server randomly fail with ECONNRESET: The socket connection was closed unexpectedly. The call never reaches the handler, so the tool does not run, and the caller cannot tell whether it did. Non-idempotent tools - messages, comments, writes - are simply lost. The rate tracks idle gaps rather than load: in one production count 798 of 100,982 loopback tool calls died this way over three and a half days. This is openclaw/openclaw#172871, fixed in PR #172980.
Why it happens
node:http closes idle keep-alive sockets after keepAliveTimeout (5 s by default) plus keepAliveTimeoutBuffer (1 s). A client with a connection pool - Bun's fetch among them - reuses a socket from that pool without knowing the server is closing it. When the reuse lands in that window the write dies, and the request is gone before any handler runs. Nothing is wrong with the tool, the payload or the response. A standalone repro with the timeout scaled down rejected 29 of 9,600 pooled fetches with the defaults, and none at all once every response asked for a close.
The fix
PR #172980 sets Connection: close on every loopback response - successes, notifications and errors alike - so the response end owns the socket shutdown and no client can reuse a socket the server is closing. It removes the race instead of nudging its timeout window. The cost is one new loopback TCP connection per request, and long-running responses and SSE streams still stay open until they end.
For any small local HTTP server where a request must not be silently lost, answer every response the same way before any branching:
res.setHeader('Connection', 'close')
The tradeoff is cheap on loopback and predictable. Leaving keep-alive on instead means tuning keepAliveTimeout above the client's pool behaviour, which only moves the race.