Rust Error Address Already in Use When Binding a Port
Binding a port that is taken fails with Address already in use, os error 98. Find the process holding it, handle TIME_WAIT and restarts, and fail with a useful message.

The server worked yesterday, today it prints Address already in use (os error 98) on startup, and nothing seems to be running. The error is exact: something owns that port. The fix takes one command to find out who. This post covers finding the process, why a restart can trip it, and how to make your program report it politely. It is an error post in the debugging cluster with broken pipe and the backtrace reading guide.
Rust 1.90, Tokio 1.
Reproduce
use std::net::TcpListener;
fn main() -> std::io::Result<()> {
let _first = TcpListener::bind("127.0.0.1:3000")?;
let _second = TcpListener::bind("127.0.0.1:3000")?;
Ok(())
}Error: Os { code: 98, kind: AddrInUse, message: "Address already in use" }The first bind succeeds and the second fails. Run two copies of a server and the same thing happens across processes.
Find the owner
lsof -i :3000 -sTCP:LISTENCOMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
demo 48211 pete 7u IPv4 0x... 0t0 TCP localhost:3000 (LISTEN)On Linux, ss -ltnp 'sport = :3000' prints the same, with the process name in the last column. kill 48211 stops it. In a container, remember the port may be taken on the host by another published container; docker ps shows port mappings.
Restart problems
A socket that was recently closed lingers in TIME_WAIT. Binding the same port without SO_REUSEADDR fails until the timer ends. Rust's std::net::TcpListener::bind sets SO_REUSEADDR on Unix, so a plain restart works; if a restart fails, an old process is still alive, often a zombie child of a dev tool like cargo watch. On Windows, SO_REUSEADDR has different semantics and the std library does not set it, so a restart can fail for a few moments.
A friendly message
use std::io::ErrorKind;
use tokio::net::TcpListener;
#[tokio::main]
async fn main() -> std::io::Result<()> {
match TcpListener::bind("0.0.0.0:3000").await {
Ok(listener) => {
println!("listening on {}", listener.local_addr()?);
Ok(())
}
Err(e) if e.kind() == ErrorKind::AddrInUse => {
eprintln!("port 3000 is in use; set PORT to another value or stop the other process");
std::process::exit(1);
}
Err(e) => Err(e),
}
}Matching on ErrorKind::AddrInUse works on every platform. Print what the user can do next.
Tests
Hard-coding a port makes tests collide when run in parallel. Bind port 0 and ask the listener where it landed:
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
let port = listener.local_addr()?.port();The operating system picks an unused port, and local_addr returns it. Pass it to the code under test. See Axum testing with oneshot for the approach that needs no socket.
Two processes on one port
SO_REUSEPORT allows several processes to bind the same port and have the kernel distribute connections. It is used for zero-downtime restarts and multi-process servers. The socket2 crate exposes it. It is also a risk: a second process that you did not intend can silently share traffic.
Limits
IPv4 and IPv6 wildcard sockets can collide in platform-specific ways (:: also covers IPv4 on some systems). Ports below 1024 need privileges and fail with PermissionDenied, a different error. The std documentation for TcpListener::bind lists the behaviors.


