07 / 08
Errors / 07

address already in use

bind() failed because something owns the port: either a live process you can name with one ss command, or the ghost of your own previous instance lingering in TIME_WAIT because the listener didn’t set SO_REUSEADDR.


The symptom

A server fails at startup trying to bind its listen port. Same error, every stack’s accent:

listen tcp :8080: bind: address already in use            (Go)
Error: listen EADDRINUSE: address already in use :::8080  (Node.js)
java.net.BindException: Address already in use            (JVM)
OSError: [Errno 98] Address already in use                (Python)

The diagnosis

1 Name the owner

$ sudo ss -tlnp "sport = :8080"
State   Recv-Q  Send-Q  Local Address:Port   Process
LISTEN  0       4096    *:8080               users:(("node",pid=4112,fd=23))
  ← without sudo you may see the socket but not the process column;
    half of all "nothing is using the port!" reports are a missing sudo

A live listener ends the mystery: decide whether to stop it (an old instance of your own service, usually) or move your port (it’s a different service that belongs there). If the Process column is empty even with sudo, the socket may belong to another network namespace — step 3. If there’s no LISTEN row at all, the port is busy without a listener — that’s TIME_WAIT, step 2.

2 No listener? Look for TIME_WAIT remnants

$ ss -tan state time-wait "( sport = :8080 )" | head -4
Recv-Q  Send-Q  Local Address:Port   Peer Address:Port
0       0       10.0.3.41:8080       10.0.9.77:51322
0       0       10.0.3.41:8080       10.0.9.80:50114
  ← connections from the PREVIOUS instance, parked for ~60s after close
    (TCP_TIMEWAIT_LEN in the kernel); they block a plain bind()

TIME_WAIT is the closing side of a TCP connection holding the address pair for about a minute so stray packets die out — see TCP’s state machine in RFC 9293. When a server that initiated closes restarts immediately, its old connections still occupy the port, and a listener that didn’t set SO_REUSEADDR can’t bind until they expire. The proof: wait sixty seconds and the bind succeeds. The fix is one socket option, not waiting.

3 Still nothing? Check other namespaces and proxies

$ sudo lsof -i :8080            # on the HOST, not in the container
COMMAND       PID  USER   TYPE  NAME
docker-pro  19204  root   IPv6  *:8080 (LISTEN)
  ← docker-proxy holds the host port for a container’s published port —
    the "invisible" owner when you only looked inside the container

Sockets are per network namespace. A container, or a process in another netns, can hold the port where your tools aren’t looking. On container hosts the usual suspect is docker-proxy (or a stale container that didn’t release its published port). Check from the host with lsof/ss, and use nsenter to inspect a specific container’s namespace when needed.

The causes, ranked

  1. 1 A live process already listens there — usually your own previous instance

    confirm ss -tlnp (with sudo) names it.

  2. 2 TIME_WAIT remnants and no SO_REUSEADDR on the listener

    confirm No LISTEN socket on the port, but ss shows time-wait rows; binding succeeds by itself about a minute later.

  3. 3 Two services configured onto the same port, or an ephemeral-port collision

    confirm The named owner is a different, legitimate service; for the rarer collision case, the owner is an outbound connection whose source port landed on your service port (check net.ipv4.ip_local_port_range).

  4. 4 A container runtime proxy holds the host port

    confirm lsof on the host shows docker-proxy (or the runtime’s equivalent) as the owner.

The fixes

A live process already listens there — usually your own previous instance

Stop it properly: systemctl stop, or SIGTERM and wait for exit. Avoid kill -9 as the routine: a KILLed parent can leave a child holding the inherited listen fd, which is how a port stays "stuck" after you killed the process you knew about. If your restart scripts race the old process, make them wait for the old PID to exit before starting the new one.

TIME_WAIT remnants and no SO_REUSEADDR on the listener

Set SO_REUSEADDR on the listening socket before bind — see socket(7). For a listener this is safe and standard (it does not allow two processes to listen at once); Go’s net.Listen and most modern frameworks already do it, hand-rolled socket code and some older runtimes don’t.

Two services configured onto the same port, or an ephemeral-port collision

Move one service. For the collision case, exclude your service ports from ephemeral allocation with net.ipv4.ip_local_reserved_ports — the fix that prevents the 3 a.m. version of this incident.

A container runtime proxy holds the host port

Remove or fix the container with that published port (docker ps with port filters finds it). Stale proxies from non-cleanly-removed containers occasionally need the container removed explicitly before the port frees.

What people get wrong

  • SO_REUSEADDR and SO_REUSEPORT are different tools. REUSEADDR lets one new listener bind past TIME_WAIT remnants — the restart fix. REUSEPORT lets multiple live sockets share a port for load distribution. Reaching for REUSEPORT to silence EADDRINUSE can leave two app instances both "successfully" listening, each receiving half the connections — the bug report reads "requests randomly hit old code".
  • kill -9 does not free ports faster. The kernel closes descriptors at process exit either way; what -9 skips is your process’s own cleanup. If a port is still busy after a -9, either a child inherited the listen socket and lives on, or you’re looking at TIME_WAIT — both findable with the steps above, neither fixed by more force.
  • "lsof shows nothing" has two boring explanations. You ran it without root (you can only see your own processes’ sockets in detail), or the socket lives in another network namespace (a container). Run it with sudo on the host before concluding anything spooky.

Quick answers

How do I find which process is using a port?

sudo ss -tlnp "sport = :PORT" — the Process column names it with PID and fd. lsof -i :PORT gives the same answer. The sudo matters: without it the owner column can be blank, which is routinely misread as "nothing is using the port".

Is SO_REUSEADDR safe to set on a server?

Yes — for TCP listeners it is standard practice and what most frameworks already do. It permits binding while old connections from a previous instance sit in TIME_WAIT; it does not let a second process steal a port with an active listener. The thing to avoid reaching for blindly is SO_REUSEPORT, which does allow multiple live listeners.

Why is the port still busy after the process died?

Two usual reasons: connections from the dead process are in TIME_WAIT for ~60 seconds and the new listener doesn’t set SO_REUSEADDR; or a child of the killed process inherited the listening descriptor and is still alive. ss -tan state time-wait and ss -tlnp distinguish them in two commands.

Related on Semicolony

Found this useful?