Ops Journal · Practical notes on running software in production

Finding which process is holding a port, and why kill does not always free it

Published 2026-10-05 · 7 min read

bind: address already in use is one of the most common errors in operations and one of the most frequently misdiagnosed. The port is held by something, and the useful work is finding out what, then understanding why it is still there.

Start with the listening sockets

ss is the modern replacement for netstat and shows what is listening, along with the process that owns each socket:

sudo ss -tulpn

The flags are worth reading one by one, because the right combination saves a lot of guessing: -t for TCP, -u for UDP, -l for listening sockets only, -p to show the owning process, and -n to print numbers instead of resolving service names and hostnames. Dropping -n makes the command slow on a host with slow DNS, and the names it prints are rarely more useful than the port number.

To answer the specific question — who has port 8080 — filter on the port:

sudo ss -tulpn | grep ':8080'

The -p flag requires root to see processes you do not own. Run it without sudo and sockets owned by other users show an empty process column, which looks like the socket has no owner. That misreading sends people looking for a phantom process that is in fact running as another user.

Confirm what a socket is actually bound to

A service bound to 127.0.0.1:8080 and one bound to 0.0.0.0:8080 look similar in a quick scan but behave completely differently: the first is reachable only from the machine itself, the second from anywhere. The local address column tells you which:

Netid State  Recv-Q Send-Q Local Address:Port Peer Address:Port Process
tcp   LISTEN 0      128          0.0.0.0:22        0.0.0.0:*     users:(("sshd",pid=712,fd=3))
tcp   LISTEN 0      511        127.0.0.1:8080      0.0.0.0:*     users:(("node",pid=4411,fd=19))

The second line is a service that cannot be reached from outside the host, which is a common cause of "it works locally but not remotely" that has nothing to do with a firewall.

When ss shows nothing and the port is still in use

This is the case that wastes the most time. ss lists no listener on the port, yet binding fails. The usual cause is a socket in TIME_WAIT, which is a normal part of TCP teardown: the side that closed first keeps the connection record for a period (twice the maximum segment lifetime) so late packets are not misinterpreted.

Sockets in this state do not appear in a listening-only listing:

ss -tan | grep ':8080'

If the output shows TIME_WAIT entries and no LISTEN, the port is not held by a process at all — it is being held by the kernel on behalf of a closed connection. Nothing needs to be killed. It clears itself, typically within a minute.

There is a legitimate fix for a server that needs to rebind immediately during development, and it belongs in the application, not in a sysctl change:

# setsockopt(SO_REUSEADDR) lets a new socket bind while old
# connections are still in TIME_WAIT. Most frameworks set this already.
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

SO_REUSEADDR is why a well-behaved server restarts cleanly while a hand-rolled one does not. It is worth checking whether your framework sets it before reaching for anything more drastic.

A process that will not die

When the owner is a real process and kill does not remove it, the sequence is worth following in order rather than jumping to -9.

First, send the default SIGTERM, which asks the process to shut down cleanly:

kill 4411

Then confirm whether it actually exited, rather than assuming:

ps -p 4411 -o pid,stat,etime,cmd

A process in state D (uninterruptible sleep) is blocked in a kernel call, usually disk I/O on a failing device or a stuck network filesystem. SIGKILL cannot interrupt D state, because the signal is delivered when the process returns to userspace and it never does. In that case the only options are to fix the underlying I/O or reboot. Killing harder will not help, and it is worth recognising the state before spending time on it.

If the process is in a normal state and ignored SIGTERM, escalate:

kill -9 4411

The trap: the parent restarts the child

A process that reappears with a new PID seconds after being killed is not failing to die. It is being respawned, and the thing to look at is its parent:

ps -o pid,ppid,cmd -p 4411

Then look at what the parent is:

ps -p <PPID> -o pid,cmd

If the parent is systemd, the unit is configured to restart and the correct action is to stop the unit rather than the process:

sudo systemctl stop myapp.service

If the parent is a supervisor like supervisord or a container runtime, the same logic applies — stop the supervisor's view of the service, or it will simply start the process again. Killing the child repeatedly is the loop that makes people conclude the process is "unkillable".

To find the unit that owns a process directly, when you know the PID:

systemctl status 4411

systemctl status accepts a PID and prints the unit it belongs to, which short-circuits the parent-chasing above.

Check the container case separately

If the port is held inside a container, the host's ss may show the published port but the owning process is in another namespace:

docker ps --format '{{.Names}}  {{.Ports}}' | grep 8080

The fix is to stop or remove the container, not to hunt for a host process. On systems where you do not have Docker socket access, ss will show the port as bound by docker-proxy or by the container runtime, which is the tell that the real owner is inside a container.

The short sequence worth memorising

sudo ss -tulpn | grep ':PORT'    # is something listening, and who
ss -tan | grep ':PORT'           # is it TIME_WAIT instead
ps -o pid,ppid,cmd -p PID        # who started it, if it keeps coming back
systemctl status PID             # which unit owns it

Work through those four in order and the port is either free or you know exactly what is holding it and why.