You turned 21 tools into one code block, cut your upfront context by 88%, and joined PostHog, VaatChitra, and Memory. Then, on run four, tool 2 throws Network connection lost. Here is why it happens, why retrying naively is hazardous, and the two-layer fix.
In the rehearsal script for your 52% Code Mode talk, there's a telling stage direction under contingencies:
"A codemode call errors (known intermittent Workers I/O bug — fix before Saturday if possible): retry once with 'even the demo believes in retries.'"
Every engineer who builds agents at the edge encounters this exact ghost. You have three tools in your bundle:
In isolated unit tests, each tool call succeeds 100% of the time. But when the LLM writes a multi-step orchestration script:
const usage = await posthog.query({ project: "site-inspection" }); // ... 4.8 seconds of model reasoning or data crunching ... const notes = await memory.find({ query: "client expectations" }); <- CRASH: fetch failed
The error arrives as TypeError: fetch failed, Network connection lost, or I/O error: connection closed before response finished. It's intermittent. It vanishes on single-tool runs. Why?
To understand this, look at how the V8 / workerdThe open-source Cloudflare Workers runtime that executes V8 isolates without Node.js operating system processes. network layer handles outbound HTTP connections.
When your Worker makes a subrequest via fetch("https://..."), it doesn't tear down the TCP/TLS connection immediately. It places the socket into an internal Keep-Alive Pool so that subsequent requests to the same host or cluster skip the 60–120ms TLS handshake.
Here is the trap: Cloudflare edge proxies and Workers frontends have aggressive idle keep-alive timeouts (often exactly 5.0 seconds).
In standard tool calling, every hop is an inference round trip, so fresh connections are negotiated. But in Code Mode, your single script executes multiple subrequests. If step 1 finishes, and data aggregation or a secondary fetch happens right around that 5-second boundary, workerd grabs the pooled socket at the exact instant the remote peer has sent a FIN.
Node.js's undici has auto-retry logic for headers-not-sent keep-alive resets. Workers fetch() does not. It surfaces directly as an unhandled I/O exception.
Adjust the execution gap between Tool 1 and Tool 2. See how standard fetch() behaves against workerd's socket pool, and what happens when the idle timeout strikes:
Under the new 2026-07-28 MCP Streamable HTTP binding, tools can stream progress or return chunked JSON-RPC streams.
If your Code Mode runner reads an MCP response but doesn't explicitly drain or cancel the body:
// NAIVE MCP CALLER IN CODEMODE const res = await fetch(serverUrl, { method: "POST", body: jsonRpc }); const data = await res.json(); // res.body is parsed, but the underlying HTTP/2 stream reader is NOT explicitly closed!
In Cloudflare Workers, an unclosed ReadableStream keeps the subrequest handle open in the V8 isolate microtask loop. When tool 2 or tool 3 executes, the isolate hits Workers' concurrent subrequest limit (50 subrequests) or stalls because connection slots are pinned.
Every response body in an edge Code Mode runner must be either fully consumed or explicitly canceled via await res.body?.cancel() before the runner resolves.
You don't need a heavy external retry framework. The fix has two crisp layers:
Look at your bundle: VaatChitra and Memory are Cloudflare Workers you control!
Calling your own Workers over public HTTPS (https://vaatchitra.prashamhtrivedi.app/mcp) is paying the network tax twice: DNS resolution, edge ingress, TLS termination, and socket pooling.
Cloudflare Workers Service Bindings with RPC (via WorkerEntrypoint) allow direct memory-speed isolate-to-isolate calls:
// In your Worker wrangler.toml: [[services]] binding = "VAATCHITRA" service = "vaatchitra-worker" [[services]] binding = "MEMORY" service = "agent-memory-worker"
| Metric | Over Public HTTPS | Via Service Binding RPC |
|---|---|---|
| Latency | 45ms – 110ms | < 0.5ms (in-process) |
| Socket Re-use | Stale RST hazard | No sockets / In-memory Cap'n Proto |
| Subrequest Quota | Consumes 1 of 50 | Zero (exempt) |
| Auth Verification | Bearer token unwrap per hop | Direct capability binding |
For external servers like PostHog that must cross public HTTPS, use a targeted wrapper that detects keep-alive socket drops before bytes were accepted and transparently retries with a fresh socket:
/** * Safe subrequest caller for Cloudflare Workers Code Mode. * - Evicts stale keep-alive sockets on connection reset. * - Guaranteed stream drainage to prevent socket pool poisoning. */ export async function safeMcpFetch(url: string, init: RequestInit, attempts = 2): Promise<Response> { for (let i = 0; i < attempts; i++) { try { const res = await fetch(url, { ...init, // Prevent proxy keep-alive staleness if this is a retried attempt headers: { ...init.headers, "Connection": i > 0 ? "close" : "keep-alive", } }); return res; } catch (err: any) { // Catch socket reset before HTTP response headers received const isSocketDrop = err?.message?.includes("network connection lost") || err?.message?.includes("fetch failed"); if (i < attempts - 1 && isSocketDrop) { // Transparent retry with a fresh TCP handshake continue; } throw err; } } throw new Error("Unreachable"); }
An agent script executes tool 1 (PostHog), performs 6 seconds of synchronous data mapping, and then calls tool 2 (VaatChitra over HTTPS). The second call throws a connection error. What happened?
The intermittent I/O bug in edge Code Mode isn't random: it is a keep-alive socket race on sequential subrequests across the 5-second proxy boundary — cured by routing internal servers through Service Bindings RPC and guarding external calls with a transparent RST retry.
Deploy this before Saturday's meetup, and you can strike the contingency line from your rehearsal script: the demo won't need to believe in retries because the socket pool won't lie to it.