cloudflare / cloudflare/workers-sdk
🐛 BUG: wrangler dev 100% CPU spin when IPv6 is disabled — Node.js binds [::1] but workerd resolves localhost to 127.0.0.1
- Dominant language
- TypeScript
- Stars
- 4.5k
- Forks
- 1.5k
- Avg merge
- 3d 8h
- Merged PRs (30d)
- 186
Description
### Which Cloudflare product(s) does this pertain to?
Wrangler
### What version(s) of the tool(s) are you using?
wrangler: 4.73.0 (also reproduced on 4.72.0)
workerd: 1.20260312.1 (also reproduced on 1.20260310.1)
### What version of Node are you using?
22.22.0
### What operating system and version are you using?
NixOS Linux (kernel 6.13.6)
### Describe the Bug
#### Observed behavior
When IPv6 is disabled on the host network, `wrangler dev` starts and prints "Ready on http://localhost:8787" but **never responds to any HTTP requests**. The main `workerd` process immediately consumes **100% CPU** and spins indefinitely. All endpoints (including static doc routes that don't touch any bindings) time out.
Debug logs (`--log-level debug`) show a tight infinite loop of:
```
exception = kj/async-io-unix.c++:1538: disconnected: connect(): Connection refused
uncaught exception; source = Uncaught (in promise); stack = Error: Network connection lost.
```
This repeats hundreds of times per second with **no backoff**, starving the workerd event loop so it cannot process any HTTP requests.
#### Root cause
The wrangler dev architecture uses two processes:
- A **Node.js process** (wrangler CLI) that listens on an internal loopback port
- A **workerd process** configured with `--external-addr=loopback=localhost:`
The bug is an **IPv4/IPv6 address mismatch** between the two:
1. Node.js binds its internal loopback port to **`[::1]` (IPv6 only)** — confirmed via `ss -tlnp`
2. workerd resolves `localhost` to **`127.0.0.1` (IPv4)** first
3. The IPv4 connection is refused (nothing listening on `127.0.0.1:`)
4. workerd retries immediately in a **tight loop with no backoff** → 100% CPU
Evidence:
```
# Node binds IPv6 only:
$ ss -tlnp | grep
LISTEN 0 511 [::1]: [::]:* users:(("node",pid=...,fd=...))
# IPv4 fails, IPv6 succeeds:
$ curl http://127.0.0.1:/ → exit 7 (connection refused)
$ curl http://[::1]:/ → exit 0 (success)
# workerd uses localhost which resolves to IPv4 first:
$ cat /proc//cmdline | tr '\0' ' ' | grep -oP 'loopback=[^ ]+'
loopback=localhost:
```
#### Expected behavior
`wrangler dev` should work correctly when IPv6 is disabled on the host. Specifically:
1. Node.js and workerd should agree on the loopback address (both use `127.0.0.1`, or use `0.0.0.0`/dual-stack)
2. If the loopback connection fails, workerd should implement **exponential backoff** instead of a tight retry loop
3. Alternatively, wrangler should pass an explicit IP address (`127.0.0.1`) instead of `localhost` for `--external-addr=loopback=`
#### Workaround
Forcing Node.js to prefer IPv4 resolves the issue:
```bash
NODE_OPTIONS="--dns-result-order=ipv4first" wrangler dev
```
This makes Node.js bind to `127.0.0.1` instead of `[::1]`, matching what workerd expects.
### Steps to reproduce
1. Disable IPv6 on your network interface (e.g., via NetworkManager, sysctl `net.ipv6.conf.all.disable_ipv6=1`, or router settings)
2. Ensure `/etc/hosts` still has both `127.0.0.1 localhost` and `::1 localhost` (default on most distros)
3. Create any Cloudflare Worker project with `nodejs_compat` enabled
4. Run `wrangler dev`
5. Try `curl http://localhost:8787/` — it will hang indefinitely
6. Check CPU: `ps aux | grep workerd` shows ~100% CPU usage
### Related issues
- #7072 — Neon DB via WebSockets hangs (similar symptom: connection refused loop with `nodejs_compat`)
- #7635 — Worker hangs after first request (ProxyWorker pause/play race)
- cloudflare/workerd#4623 — macOS dual-stack "happy eyeballs" crash (same family of IPv4/IPv6 mismatch bugs, fixed for macOS via capnproto/capnproto#2389)
### Please provide any relevant error logs
Debug output (`wrangler dev --log-level debug`) shows the repeating pattern immediately at startup, before any HTTP request:
```
exception = kj/async-io-unix.c++:1538: disconnected: connect(): Connection refused
stack: ...workerd@4f45f62 ...workerd@4f461eb ...
uncaught exception; source = Uncaught (in promise); stack = Error: Network connection lost.
uncaught exception; exception = workerd/jsg/_virtual_includes/iterator/workerd/jsg/value.h:1480: disconnected: jsg.Error: Network connection lost.
stack: ...workerd@2cfbe50 ...workerd@2cfca8e ...
```
This pattern repeats hundreds of times per second with no backoff.
### Environment details
```
wrangler: 4.73.0
workerd: 1.20260312.1
node: 22.22.0
bun: 1.3.9
OS: NixOS Linux, kernel 6.13.6
IPv6: disabled on network interfaces
/etc/hosts: both 127.0.0.1 and ::1 mapped to localhost
```
Contributor guide
Assessment
This issue has not been assessed yet.