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
- Choose a free port: mdbook serve -p 3001 (or any unused port).
- Find and stop the process holding the port: lsof -i :3000 or ss -ltnp, then kill it.
- Bind to a valid local interface: use --hostname 127.0.0.1 or 0.0.0.0 instead of an unassigned IP.
- 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
- Reserve a dedicated port for mdbook serve and check it with lsof/ss first.
- Terminate stale mdbook serve processes before restarting.
- Use ports >= 1024 to avoid permission issues.
- Bind to 127.0.0.1 or 0.0.0.0 unless a specific interface is required.
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.