Skip to content
rusthacks.dev
Tooling

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.

3 min read
Summarize in AI
Flat illustration for the post “Rust Error Address Already in Use When Binding a Port”, composed around ffi.

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

rust
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(())
}
Text
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

Shell
lsof -i :3000 -sTCP:LISTEN
Text
COMMAND   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

rust
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:

rust
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.

Also inWeb
Taggedtcpbind
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.