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

  1. Capture and read the child's stderr/stdout — exec failures and early crashes print there; fix whatever it reports (missing binary, bad args).
  2. Verify the child binary exists, is executable, and is the same build/arch/version as the parent.
  3. Increase the spawn timeout so slow or loaded machines still fit inside the deadline.
  4. 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

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

Related errors


AI-assisted analysis of facebook/flow@f88ac94bcf (2026-08-20). Data as JSON: /api/errors/f4b1ccc1c091d07b. Report an issue: GitHub.