rust-lang/mdBook · critical

Unable to bind to {address}: {e}

Error message

Unable to bind to {address}: {e}

What it means

mdbook serve binds a tokio TcpListener on the configured address before serving the axum app. If the OS refuses the bind (address/port already in use, no permission, invalid address), the async bind returns an error and the code panics with `Unable to bind to {address}: {e}`. The message embeds both the attempted address and the underlying io::Error.

Source

Thrown at src/cmd/serve.rs:137

        let reload_tx = reload_tx_clone.clone();
        ws.on_upgrade(move |socket| websocket_connection(socket, reload_tx))
    };

    let app = Router::new()
        .route(&format!("/{LIVE_RELOAD_ENDPOINT}"), get(websocket_handler))
        .fallback_service(
            ServeDir::new(&build_dir).not_found_service(ServeFile::new(build_dir.join(file_404))),
        );

    std::panic::set_hook(Box::new(move |panic_info| {
        // exit if serve panics
        error!("Unable to serve: {}", panic_info);
        std::process::exit(1);
    }));

    let listener = tokio::net::TcpListener::bind(&address)
        .await
        .unwrap_or_else(|e| panic!("Unable to bind to {address}: {e}"));

    axum::serve(listener, app).await.unwrap();
}

async fn websocket_connection(ws: WebSocket, reload_tx: broadcast::Sender<Message>) {
    let (mut user_ws_tx, _user_ws_rx) = ws.split();
    let mut rx = reload_tx.subscribe();

    trace!("websocket got connection");
    if let Ok(m) = rx.recv().await {
        trace!("notify of reload");
        let _ = user_ws_tx.send(m).await;
    }
}

View on GitHub (pinned to dc21064fc2)

Solutions

  1. Choose a free port: mdbook serve -p 3001 (or any unused port).
  2. Find and stop the process holding the port: lsof -i :3000 or ss -ltnp, then kill it.
  3. Bind to a valid local interface: use --hostname 127.0.0.1 or 0.0.0.0 instead of an unassigned IP.
  4. For privileged ports, either use a port >=1024 or run with the necessary privileges/capabilities.

Example fix

// before
mdbook serve --hostname 192.168.1.99 -p 3000

// after
mdbook serve --hostname 127.0.0.1 -p 3001
Defensive patterns

Strategy: try-catch

Validate before calling

// check port availability before launching serve
use std::net::TcpListener;
fn port_free(addr: &str) -> bool {
    TcpListener::bind(addr).is_ok()
}
if !port_free("127.0.0.1:3000") {
    eprintln!("port 3000 in use; pick another with mdbook serve -p <port>");
}

Try / catch

// run mdbook serve as a child process and interpret the panic
let status = Command::new("mdbook")
    .args(["serve", "-p", &port.to_string()])
    .status()?;
if !status.success() {
    eprintln!("serve failed to start (bind error?); try a different port");
}

Prevention

When it happens

Trigger: Running `mdbook serve` on a port already occupied by another process, binding to a privileged port (<1024) without permissions, specifying an IP address not assigned to any local interface, or starting two `mdbook serve` instances concurrently.

Common situations: Port 3000 already used by another dev server, a previous mdbook serve instance that didn't exit, Docker/WSL port conflicts, or --hostname set to a public IP the machine doesn't own.

Related errors


AI-assisted analysis of rust-lang/mdBook@dc21064fc2 (2026-09-01). Data as JSON: /api/errors/df7f027eb98e524b. Report an issue: GitHub.