What versions & operating system are you using?
System:
OS: Linux 7.0 EndeavourOS
CPU: (16) x64 13th Gen Intel(R) Core(TM) i7-1360P
Memory: 11.24 GB / 15.30 GB
Container: Yes
Shell: 5.9 - /usr/bin/zsh
Binaries:
Node: 26.1.0 - /home/fuyu/.local/share/mise/installs/node/26.1.0/bin/node
Please provide a link to a minimal reproduction
N/A
Describe the Bug
Summary
When workerd exits during startup before writing all expected
{"event":"listen"} / {"event":"listen-inspector"} messages to the control
file descriptor (FD3), Miniflare's internal waitForPorts() blocks forever.
From the user's perspective, wrangler dev stalls at:
⎔ Starting local server...
with no error, no timeout, and no way to diagnose the failure without external
tools like strace.
Environment
- OS: EndeavourOS Linux (
kernel 7.0.10-arch1-1)
- Node.js:
v26
- Wrangler:
4.95.0
- Miniflare:
4.20260526.0
Steps to Reproduce
One concrete trigger, though there may be others:
-
Disable IPv6 at the kernel level:
net.ipv6.conf.all.disable_ipv6 = 1
-
Leave the default /etc/hosts entry intact:
::1 localhost ip6-localhost ip6-loopback
-
Run:
wrangler dev prints the following and never proceeds:
⎔ Starting local server...
Root Cause Analysis
What Miniflare does
Miniflare spawns workerd with:
--inspector-addr=localhost:0
localhost is hardcoded in:
packages/miniflare/src/index.ts:2432
Miniflare then reads the control FD, FD3, line-by-line, waiting for listen
events from all required sockets:
{"event":"listen","socket":"entry","port":8787}
{"event":"listen-inspector","port":12345}
The inspector socket uses a separate event type, listen-inspector.
waitForPorts() resolves only when all expected events arrive.
What goes wrong
--inspector-addr=localhost:0 is passed to workerd.
workerd calls getaddrinfo("localhost").
- RFC 6724 address selection prefers
::1 over 127.0.0.1 when both appear
in /etc/hosts.
workerd attempts bind(::1, 0) for the inspector socket.
- With IPv6 disabled at the kernel level,
::1 is not available, so the bind
fails.
workerd exits without writing the listen-inspector event to FD3.
- Miniflare's
waitForPorts() is still waiting for the inspector event, but
the process is already dead.
The missing piece
waitForPorts() does not race against the workerd process exit event.
If workerd exits for any reason before all listen events are written,
Miniflare has no way to detect this and hangs indefinitely.
There is also no error event defined in the FD3 control protocol, so workerd
has no mechanism to signal startup failure to Miniflare.
Secondary gap: handleStartupFailure() only covers one case
packages/miniflare/src/runtime/index.ts:198-212 currently inspects
workerd's stderr only for the string:
An IPv6 bind failure produces a different message.
Therefore, even if waitForPorts() returned undefined rather than hanging,
the caller would receive only the generic message:
The Workers runtime failed to start. There is likely additional logging output above.
This is still insufficient for diagnosis.
Expected Behavior
When workerd exits during startup, Miniflare should immediately reject with
an error like:
Error: workerd exited (code=1) before all sockets were ready.
Required: entry, inspector. Received: entry.
Proposed Fix
The fix belongs in updateConfig() in:
packages/miniflare/src/runtime/index.ts
This is where the process reference is available.
Race waitForPorts() against the child process exit:
// In Runtime#updateConfig(), after writing the config to stdin:
const ports = await Promise.race([
waitForPorts(controlPipe, options),
new Promise<never>((_, reject) => {
runtimeProcess.once("exit", (code, signal) => {
reject(
new MiniflareCoreError(
"ERR_RUNTIME_FAILURE",
`workerd exited (code=${code ?? signal}) before all sockets were ready.`
)
);
});
}),
]);
Note: kInspectorSocket is a Symbol, so the error message should map it to a
human-readable name, such as inspector, rather than emitting
Symbol(kInspectorSocket).
This fix is robust because it handles any workerd startup failure regardless
of cause, including:
- port conflict
- missing binary
- bind error
- permission denied
- other startup-time failures
Notes
- The immediate workaround for the IPv6 case is to comment out
::1 localhost
in /etc/hosts, so that localhost resolves only to 127.0.0.1.
- A separate improvement would be for
workerd to write an error event to FD3
on startup failure, so that the control protocol has explicit failure
semantics.
handleStartupFailure() should also be extended to surface other startup
failure patterns from workerd's stderr, not just "Address already in use".
- Related:
#4866 (workerd not cleaned up on wrangler error)
- Related:
#6510 orphaned workerd after port bind failure
Please provide any relevant error logs
No response
What versions & operating system are you using?
System:
OS: Linux 7.0 EndeavourOS
CPU: (16) x64 13th Gen Intel(R) Core(TM) i7-1360P
Memory: 11.24 GB / 15.30 GB
Container: Yes
Shell: 5.9 - /usr/bin/zsh
Binaries:
Node: 26.1.0 - /home/fuyu/.local/share/mise/installs/node/26.1.0/bin/node
Please provide a link to a minimal reproduction
N/A
Describe the Bug
Summary
When
workerdexits during startup before writing all expected{"event":"listen"}/{"event":"listen-inspector"}messages to the controlfile descriptor (FD3), Miniflare's internal
waitForPorts()blocks forever.From the user's perspective,
wrangler devstalls at:with no error, no timeout, and no way to diagnose the failure without external
tools like
strace.Environment
kernel 7.0.10-arch1-1)v264.95.04.20260526.0Steps to Reproduce
One concrete trigger, though there may be others:
Disable IPv6 at the kernel level:
Leave the default
/etc/hostsentry intact:Run:
wrangler devprints the following and never proceeds:Root Cause Analysis
What Miniflare does
Miniflare spawns
workerdwith:localhostis hardcoded in:Miniflare then reads the control FD, FD3, line-by-line, waiting for listen
events from all required sockets:
{"event":"listen","socket":"entry","port":8787}{"event":"listen-inspector","port":12345}The inspector socket uses a separate event type,
listen-inspector.waitForPorts()resolves only when all expected events arrive.What goes wrong
--inspector-addr=localhost:0is passed toworkerd.workerdcallsgetaddrinfo("localhost").::1over127.0.0.1when both appearin
/etc/hosts.workerdattemptsbind(::1, 0)for the inspector socket.::1is not available, so the bindfails.
workerdexits without writing thelisten-inspectorevent to FD3.waitForPorts()is still waiting for the inspector event, butthe process is already dead.
The missing piece
waitForPorts()does not race against theworkerdprocess exit event.If
workerdexits for any reason before all listen events are written,Miniflare has no way to detect this and hangs indefinitely.
There is also no error event defined in the FD3 control protocol, so
workerdhas no mechanism to signal startup failure to Miniflare.
Secondary gap:
handleStartupFailure()only covers one casepackages/miniflare/src/runtime/index.ts:198-212currently inspectsworkerd's stderr only for the string:An IPv6 bind failure produces a different message.
Therefore, even if
waitForPorts()returnedundefinedrather than hanging,the caller would receive only the generic message:
This is still insufficient for diagnosis.
Expected Behavior
When
workerdexits during startup, Miniflare should immediately reject withan error like:
Proposed Fix
The fix belongs in
updateConfig()in:This is where the process reference is available.
Race
waitForPorts()against the child process exit:Note:
kInspectorSocketis aSymbol, so the error message should map it to ahuman-readable name, such as
inspector, rather than emittingSymbol(kInspectorSocket).This fix is robust because it handles any
workerdstartup failure regardlessof cause, including:
Notes
::1 localhostin
/etc/hosts, so thatlocalhostresolves only to127.0.0.1.workerdto write an error event to FD3on startup failure, so that the control protocol has explicit failure
semantics.
handleStartupFailure()should also be extended to surface other startupfailure patterns from
workerd's stderr, not just"Address already in use".#4866(workerdnot cleaned up onwranglererror)#6510orphanedworkerdafter port bind failurePlease provide any relevant error logs
No response