facebook/flow · error · std::io::Error
Daemon::spawn: timed out waiting for child to connect
Error message
Daemon::spawn: timed out waiting for child to connect
What it means
Daemon::spawn sets up two loopback TCP listeners on ephemeral ports, execs the child with the ports and a 32-byte token, then polls accept() (non-blocking, 10 ms sleep) until a connection arrives, bounded by a timeout (accept_with_token in flow_daemon/src/daemon.rs). "timed out waiting for child to connect" means the deadline passed without any accepted connection: the child never reached connect() on the parent's listener.
Source
Thrown at rust_port/crates/flow_daemon/src/daemon.rs:411
fn accept_with_token(
listener: &TcpListener,
expected_token: &[u8; 32],
timeout: Duration,
) -> std::io::Result<TcpStream> {
// We must defend against a racing local process connecting to our
// ephemeral port. Loop accepting until we see a connection bearing the
// expected token; reject and close anything else. Bound by `timeout`.
//
// `TcpListener::accept` is unconditionally blocking. To honor the
// deadline we put the listener into non-blocking mode and poll. Polling
// sleeps 10ms between attempts -- a bounded busy-wait -- which is
// negligible because the child is expected to connect within milliseconds
// of exec.
listener.set_nonblocking(true)?;
let deadline = std::time::Instant::now() + timeout;
loop {
if std::time::Instant::now() >= deadline {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Daemon::spawn: timed out waiting for child to connect",
));
}
let (mut stream, _peer) = match listener.accept() {
Ok(pair) => pair,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(Duration::from_millis(10));
continue;
}
Err(e) => return Err(e),
};
// The accepted stream inherits the listener's nonblocking flag on
// some platforms; ensure it is blocking and bounded by remaining time.
stream.set_nonblocking(false)?;
stream.set_nodelay(true)?;
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
if remaining.is_zero() {View on GitHub (pinned to f88ac94bcf)
Solutions
- Capture and read the child's stderr/stdout — exec failures and early crashes print there; fix whatever it reports (missing binary, bad args).
- Verify the child binary exists, is executable, and is the same build/arch/version as the parent.
- Increase the spawn timeout so slow or loaded machines still fit inside the deadline.
- Check the environment allows loopback TCP (container/sandbox policy, firewall rules).
Example fix
// before: tight timeout, child stderr discarded let daemon = Daemon::spawn(cmd, args, Duration::from_millis(500))?; // after: keep child stderr piped for diagnosis, allow slow starts let daemon = Daemon::spawn(cmd, args, Duration::from_secs(10))?; // and pipe stderr to a log
Defensive patterns
Strategy: retry
Validate before calling
use std::process::Command;
// Cheap pre-flight before Daemon::spawn: fail fast with a clear error
// instead of waiting out the timeout when the child cannot exec at all.
fn child_can_exec(bin: &str) -> std::io::Result<()> {
Command::new(bin).arg("--version").status()?; // Err if the binary cannot be spawned
Ok(())
} Type guard
fn is_spawn_connect_timeout(e: &std::io::Error) -> bool {
e.kind() == std::io::ErrorKind::TimedOut
&& e.to_string().contains("waiting for child to connect")
} Try / catch
On TimedOut 'waiting for child to connect': log the child's stderr (pipe it, do not discard), verify the binary exists and matches the parent's version, then retry the spawn once with a larger timeout. Escalate to a 'daemon failed to start' error if the retry also times out.
Prevention
- Pin parent and child to the same binary path/version; no mixed installs on PATH.
- Pipe child stderr to a log so exec failures are diagnosable the first time.
- Budget spawn timeouts for loaded machines (seconds, not milliseconds).
- Ensure the runtime environment permits loopback TCP.
When it happens
Trigger: The spawned child binary fails to exec (missing file, bad arguments, dynamic-loader error) or crashes before connecting; the child is too slow to start (cold page cache, heavy load) and connects after the deadline; a sandbox or firewall blocks loopback TCP connections.
Common situations: Missing or mismatched daemon binary on PATH; parent and child from different installs/versions; CI sandboxes that block socket() or loopback binds; wrong-architecture binary (x86 binary on an arm host) failing instantly at exec.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Daemon::spawn: timed out reading token from child
- Daemon::from_channel: bincode deserialize
- Daemon::flush failed
- Daemon child: failed to connect to parent out-socket (child
- Daemon child: failed to set TCP_NODELAY on parent out-socket
AI-assisted analysis of facebook/flow@f88ac94bcf (2026-08-20).
Data as JSON: /api/errors/f4b1ccc1c091d07b.
Report an issue: GitHub.