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
- 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
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