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

Open
#12,910 2 comments 0 reactions 2 assignees Claimed by @MattieTK View on GitHub
package:wrangler
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.