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. ss only 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’s ss shows the port as free. Check ip netns list and 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:8080 accepts loopback traffic only. Scanning for 0.0.0.0 or 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

  1. Find the holder with sudo ss -tlnp 'sport = :8080' (add -u for UDP); netstat -tulpn only if you’re on a legacy host.
  2. Want the file descriptor or a state filter? sudo lsof -nP -iTCP:8080 -sTCP:LISTEN; kill in bulk with sudo lsof -t -iTCP:8080 -sTCP:LISTEN | xargs -r kill.
  3. Want to signal directly? sudo fuser -k -TERM 8080/tcp (SIGTERM, then escalate to SIGKILL).
  4. Port still busy after the kill? It’s TIME-WAIT — check ss -tan state time-wait; a listener with SO_REUSEADDR binds straight through it, otherwise wait ~60s. Never touch tcp_tw_recycle.
  5. Port re-occupied instantly? A supervisor restarted it — read /proc/<pid>/cgroup and stop the systemd unit, not the PID.
  6. 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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *