cloudflare / cloudflare/workers-sdk

`wrangler dev` exits with "Error inside ProxyWorker / Network connection lost" when an open WebSocket coexists with a ~5s HTTP request cadence

Open
#15,452 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
4.5k
Forks
1.5k
Avg merge
3d 8h
Merged PRs (30d)
186

Description

### What versions & operating system are you using?

- wrangler 4.126.0 (which pins `miniflare` 5.20260825.0-alpha and `@cloudflare/workerd-linux-64` 1.20260825.1)
- Node 22.22.2, Linux 6.18 x64

### Please provide a link to a minimal reproduction

Three files, no dependencies, no browser. Reproduces in 10-60s.

**`worker.js`** - a plain Worker WebSocket. No Durable Object, no assets, no bindings:

```js
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/health") return Response.json({ ok: true });
if (url.pathname === "/ws" && request.headers.get("upgrade")?.toLowerCase() === "websocket") {
const [client, server] = Object.values(new WebSocketPair());
server.accept();
server.addEventListener("message", () => server.send(JSON.stringify({ type: "ACK" })));
return new Response(null, { status: 101, webSocket: client });
}
return new Response("not found", { status: 404 });
},
};
```

**`wrangler.jsonc`**:

```jsonc
{
"name": "ws-cadence-crash",
"main": "worker.js",
"compatibility_date": "2026-08-10",
}
```

**`client.mjs`** - holds one WebSocket open and makes an ordinary HTTP request every 5s:

```js
const base = process.argv[2] ?? "http://localhost:8787";
const alive = async () => {
try {
return (await fetch(`${base}/health`, { signal: AbortSignal.timeout(2500) })).ok;
} catch {
return false;
}
};

let connects = 0;
const closes = [];
function connect() {
const ws = new WebSocket(`${base.replace("http", "ws")}/ws`);
connects++;
ws.addEventListener("open", () => ws.send("hello"));
ws.addEventListener("close", (e) => {
closes.push(e.code);
setTimeout(connect, 500);
});
ws.addEventListener("error", () => {});
}
connect();

// Remove this line and the crash does not happen.
const poll = setInterval(() => fetch(`${base}/health`).catch(() => {}), 5000);

for (let s = 10; s <= 120; s += 10) {
await new Promise((r) => setTimeout(r, 10_000));
const up = await alive();
console.log(
`${s}s: connects=${connects} closeCodes=${JSON.stringify(closes)} devServerAlive=${up}`,
);
if (!up) {
console.log("REPRODUCED: wrangler dev exited");
clearInterval(poll);
process.exit(1);
}
}
clearInterval(poll);
console.log("not reproduced in 120s");
process.exit(0);
```

```bash
npx wrangler dev --port 8787
# in another shell:
node client.mjs http://localhost:8787
```

Observed:

```
10s: connects=1 closeCodes=[] devServerAlive=true
20s: connects=1 closeCodes=[] devServerAlive=true
30s: connects=2 closeCodes=[1006] devServerAlive=false
REPRODUCED: wrangler dev exited
```

### Describe the Bug

With a WebSocket open through `wrangler dev`, an ordinary HTTP request cadence of roughly 5 seconds kills the dev server. The socket is dropped with code `1006`, and `wrangler dev` prints an empty `✘ [ERROR]` and exits. Every request after that is refused.

The debug log has the real error:

```
Error in ProxyController: Error inside ProxyWorker
at castErrorCause (.../wrangler-dist/cli.js)
at ProxyController2.emitErrorEvent
at ProxyController2.onProxyWorkerMessage
cause: {
name: 'Error',
message: 'Network connection lost.',
stack: 'Error: Network connection lost.'
}
```

**The cadence is the trigger, and it is specifically ~5s.** Same worker, same client, only the poll interval changed:

| Poll interval | Result |
| ------------------ | ------------------------------------- |
| none (socket only) | alive at 120s, 1 connection, no drops |
| 1s | alive at 90s, 1 connection, no drops |
| **5s** | **dead in 30s**, socket dropped 1006 |
| 20s | alive at 120s |

That 5s number, and the phase-locking framing, match #14641 ("steady ~5s request cadence phase-locks with kj's 5s keep-alive timeouts"). The difference here is the outcome: with a WebSocket open the failure is not an intermittent 503 on one request, it is fatal for the whole dev server.

**What is not required.** I reached this by stripping a real application down, and each of these was eliminated by experiment - the crash survives their removal:

- Durable Objects (the version above is a plain Worker; a hibernatable-`acceptWebSocket` DO behaves identically)
- `assets` (this distinguishes it from #15203, where the same signature goes away without `assets`)
- a browser (pure Node above; it first showed up in Chromium)
- WebSocket traffic - the socket can be idle after one frame
- a POST or a request body - plain GETs are enough
- `nodejs_compat`, `observability`, D1, DO-to-DO RPC, alarms, storage

**Relationship to existing issues.** #15317 reports that `Error inside ProxyWorker` is hardcoded as fatal and that the printed message is empty; both apply here. #15203 is the same signature from a different trigger (POST with body + `assets`). This report is a third route to it, and the simplest one I could construct: one WebSocket plus a 5s GET cadence, on a Worker with no bindings at all.

If the fatality is addressed per #15317, this particular route stops taking the dev server down - but the dropped `1006` under a 5s cadence would presumably remain.

### Please provide any relevant error logs

Reproduced 6+ times across variants on wrangler 4.126.0. The `✘ [ERROR]` line printed to the console is always empty; the `Network connection lost.` cause above is only in `~/.config/.wrangler/logs/wrangler-*.log` at debug level.

Contributor guide

Open the contributing guide

Research direction

Reproduce the failure with worker.js, wrangler.jsonc, and client.mjs using the documented wrangler dev commands, then inspect the debug log for the ProxyController/ProxyWorker error. Compare the behavior with the related issues #14641, #15317, and #15203. Done means the WebSocket and 5-second HTTP cadence no longer cause wrangler dev to exit or refuse subsequent requests.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js, typescript
Domain
cli, devtools, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.