Find and Kill the Process Holding a Port on Linux
Find and Kill the Process Holding a Port on Linux
It’s the worst time of day for it: you’ve just deployed, nginx is down, and the restart loop is spitting out bind() to 0.0.0.0:8080 failed (98: Address already in use). Something is sitting on your port and you need to find the process using a port on Linux, work out who owns it, and clear it without taking down the wrong thing. This is the drill — ss first, lsof and fuser as backup — plus the two traps that turn a five-second fix into an hour of confusion.
Why ss is the tool you should reach for first
ss ships with iproute2, which is installed everywhere on Debian 12/13 and Ubuntu 22.04/24.04. netstat comes from net-tools, which those releases no longer install by default. If netstat works on your box, someone installed the legacy package. Prefer ss: it talks to the kernel over netlink instead of parsing /proc/net/tcp, so it’s faster and it doesn’t bog down under tens of thousands of sockets.
The workflow is always the same three flags. -t for TCP, -l for listening sockets only, -n to keep ports and addresses numeric, and -p to attach the owning process:
$ sudo ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1043,fd=6))
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=902,fd=3))
LISTEN 0 511 127.0.0.1:8080 0.0.0.0:* users:(("node",pid=21437,fd=23))
-p needs root (or CAP_NET_ADMIN) to see sockets owned by other users. As a normal user you’ll get the socket rows with an empty Process column. Run it under sudo.
You rarely want the whole table. Filter with a quoted expression — the quotes matter, because the filter contains spaces:
$ sudo ss -tlnp 'sport = :8080'
LISTEN 0 511 0.0.0.0:8080 0.0.0.0:* users:(("node",pid=21437,fd=23))
Add -u when the port might be UDP; -tulnp covers both. Add -H to drop the header line so the output is script-friendly. The users:((...)) field is your answer: that’s the process name, PID, and the file descriptor it holds the socket on.
netstat, if you’re on an old box
On a legacy host, netstat -tulpn gives the equivalent picture. The -p column shows PID/Program name:
$ sudo netstat -tlnp
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 21437/node
Two caveats. net-tools is deprecated and gone from fresh installs, so don’t build tooling on it. And on some systems a stub netstat from iproute2 shadows the net-tools one on $PATH, so the flags you remember may not behave. If netstat surprises you, check command -v netstat and dpkg -S $(command -v netstat).
lsof: mapping a port to the exact process and file descriptor
When you want more than the PID — the file descriptor, the socket type, the state — lsof is the sharp instrument. The -n and -P flags stop it resolving hostnames and service names, which keeps it fast and unambiguous:
$ sudo lsof -nP -i :8080
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 21437 liam 23u IPv4 39284 0t0 TCP *:8080 (LISTEN)
To see listeners only, filter by state — -i :8080 also matches clients connecting *to* port 8080, which is a common source of “why does this PID look wrong”:
$ sudo lsof -nP -iTCP:8080 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 21437 liam 23u IPv4 39284 0t0 TCP *:8080 (LISTEN)
lsof -t prints PIDs only, which is built for piping. To kill every process on a port in one shot:
$ sudo lsof -t -iTCP:8080 -sTCP:LISTEN | xargs -r kill
$ sudo lsof -t -iTCP:8080 -sTCP:LISTEN | xargs -r kill -9
The -r on xargs matters: without it, xargs runs kill with no arguments when lsof finds nothing.
fuser: when you want to signal instead of look
fuser is the one-liner for “whoever owns this port, kill it.” It takes the port as PORT/proto or via -n:
$ sudo fuser -v 8080/tcp
USER PID ACCESS COMMAND
8080/tcp: liam 21437 F.... node
Note that fuser prints PIDs to stdout and its diagnostics to stderr — a detail that bites people piping it into scripts. To send a signal, use -k; the default is SIGKILL, which is usually not what you want as a first move:
$ sudo fuser -k -TERM 8080/tcp
$ sudo fuser -k 8080/tcp # SIGKILL
Same shape with the -n form: fuser -n tcp -k 8080.
The trap: the port is not released the instant you kill the process
You killed the PID. You re-run ss -tlnp and there’s no listener. You start your service and get Address already in use again. Nothing is listening — so what’s holding the port?
Time-wait. Every TCP connection that the killing process had closed stays in the TIME-WAIT state for a fixed interval — 60 seconds on Linux (2× the maximum segment lifetime) — to absorb late duplicate packets. TIME-WAIT sockets carry the server’s local port, so they show up on port 8080. Crucially they don’t match ss -tln (they aren’t listening), but they do block a plain bind(). Look with -a:
$ sudo ss -tan state time-wait '( sport = :8080 )'
State Recv-Q Send-Q Local Address:Port Peer Address:Port
TIME-WAIT 0 0 10.0.0.5:8080 203.0.113.9:51344
TIME-WAIT 0 0 10.0.0.5:8080 198.51.100.7:44201
The fix is almost always the server’s job, not yours: software that sets SO_REUSEADDR — nginx, Apache, systemd socket units, and most language HTTP servers do — binds straight through TIME-WAIT sockets. If yours can’t, either wait the 60 seconds, or let it reuse the socket. Don’t reach for net.ipv4.tcp_tw_recycle: it was removed in kernel 4.12 and is a no-op on any modern Debian 12/13 or Ubuntu 22.04/24.04. net.ipv4.tcp_tw_reuse exists and its default already depends on your kernel version — check before changing it, and prefer fixing the listener.
The trap that actually wastes your afternoon: something restarts it
Here is the detail most guides skip. If the port is re-occupied within seconds of your kill, the process didn’t survive — a supervisor resurrected it. Kill the PID and systemd’s Restart=always, a socket unit, or a process manager (supervisord, pm2) simply starts a new one that grabs the same port. You’ll loop forever chasing PIDs.
Find the owner before you kill:
$ sudo ss -tlnp 'sport = :8080'
LISTEN 0 511 0.0.0.0:8080 0.0.0.0:* users:(("node",pid=21437,fd=23))
$ cat /proc/21437/cgroup
0::/system.slice/myapp.service
That cgroup path names the systemd unit. Stop the unit, not the PID:
$ sudo systemctl stop myapp.service
$ sudo systemctl disable --now myapp.service # if it must not come back
The same reflex applies to a PID you can’t place by name: systemctl status <PID> resolves it to a unit, and systemd-cgls shows the whole tree.
Port looks free in ss, but the connection is still refused
You run ss -tlnp, see nothing on 8080, and yet clients get “connection refused” — or a service that claims to be listening can’t be reached. Three causes, in order of how often they bite:
- Network namespaces.
ssonly reports sockets in the namespace you’re in. A process in a container or a dedicated netns holds the port in its namespace while the host’sssshows the port as free. Checkip netns listand inspect inside it:sudo ip netns exec app1 ss -tlnp. For a container, enter its namespace:sudo nsenter -t $(docker inspect -f '{{.State.Pid}}' web) -n ss -tlnp. - A specific bind address. A socket bound to
127.0.0.1:8080accepts loopback traffic only. Scanning for0.0.0.0or grepping the wrong string makes it look like the public port is free; connections to the external IP are refused while the service happily serves localhost. - Wrong protocol or IP family. You checked TCP, the socket is UDP (
ss -ulnp); or you checked-4, and the listener is on IPv6 ([::]:8080, shown by-6). A dual-stack IPv6 socket can serve IPv4 too, so it can hold the port without appearing in your IPv4-only view.
When ss says free but a client is refused, stop trusting the filter and widen it: sudo ss -tulnp for everything, then sudo ss -tan to see non-listening states, then check other namespaces.
The short version
- Find the holder with
sudo ss -tlnp 'sport = :8080'(add-ufor UDP);netstat -tulpnonly if you’re on a legacy host. - Want the file descriptor or a state filter?
sudo lsof -nP -iTCP:8080 -sTCP:LISTEN; kill in bulk withsudo lsof -t -iTCP:8080 -sTCP:LISTEN | xargs -r kill. - Want to signal directly?
sudo fuser -k -TERM 8080/tcp(SIGTERM, then escalate to SIGKILL). - Port still busy after the kill? It’s TIME-WAIT — check
ss -tan state time-wait; a listener withSO_REUSEADDRbinds straight through it, otherwise wait ~60s. Never touchtcp_tw_recycle. - Port re-occupied instantly? A supervisor restarted it — read
/proc/<pid>/cgroupand stop the systemd unit, not the PID. - Port looks free but connections are refused? You’re in the wrong namespace, the wrong bind address, or the wrong protocol/family. Widen the filter.