Linux

Find Which Process Is Listening on a Port in Linux

By Nawawi JamiliPublished 6 min read

"Address already in use." At some point, every service you start on Linux fails with that message. The next question is always the same: what is already using that port? This post covers the three tools I use (ss, lsof and fuser). It also covers reading /proc/net/tcp directly when none of them is installed. Finally, it explains why all of them sometimes show you a port but not its owner.

To have something to find, I started a small web server bound to the loopback address on port 8731:

terminal
$ python3 -m http.server 8731 --bind 127.0.0.1 &

All output below is from an EL8 server with iproute2 6.2 and lsof 4.93, run as an ordinary user. Your PIDs and port lists will differ.

ss: the modern default

ss comes with iproute2 and replaces the old netstat, which many minimal installs no longer include. The flags I type almost every day are -ltnp:

  • -l: listening sockets only
  • -t: TCP (use -u for UDP, or both)
  • -n: numeric ports and addresses, no DNS or /etc/services lookups
  • -p: show the process that owns each socket
terminal
$ ss -ltnp
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      65536      127.3.2.1:3306      0.0.0.0:*
LISTEN 0      128          0.0.0.0:111       0.0.0.0:*
LISTEN 0      65536      127.3.2.1:80        0.0.0.0:*
LISTEN 0      65536      127.3.2.1:8025      0.0.0.0:*
LISTEN 0      5          127.0.0.1:8731      0.0.0.0:*    users:(("python3",pid=152425,fd=3))
LISTEN 0      511        127.0.0.1:8091      0.0.0.0:*    users:(("node",pid=120638,fd=18))
LISTEN 0      65536      127.3.2.1:443       0.0.0.0:*
LISTEN 0      128             [::]:111          [::]:*

The Python server is there, with its command name, PID and file descriptor. On a listening socket, Send-Q is the accept backlog, and 5 is also the default queue size of Python's socketserver. The Local Address column tells you where it listens: 127.0.0.1 is reachable only from this machine, while 0.0.0.0 or [::] means every interface. That column has answered more "I cannot connect from outside" questions for me than any firewall rule.

Rather than grep, you can give ss a filter expression:

terminal
$ ss -ltnp 'sport = :8731'
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      5          127.0.0.1:8731      0.0.0.0:*    users:(("python3",pid=152425,fd=3))

Quote the filter so the shell does not change it. grep ':8731' works too, but it also matches 18731 and any peer address that contains the string.

Why most of the ports have no process

Look again at the full list: ports 3306, 80, 443 and others have an empty process column. They do not lack an owner. They belong to processes running as other users (root, here), and an unprivileged user is not allowed to look inside those processes.

The reason is how these tools work. The kernel's socket table records the local and remote address, the state, the owning UID and an inode number, but not a PID. To map a socket to a process, ss, lsof and fuser search /proc/<pid>/fd/ for every process, looking for a file descriptor that points at that socket inode. Those fd directories are readable only by the process owner (and root). So as a normal user you see every socket on the system, but only your own processes behind them.

The fix is simply to run the command with root privileges: sudo ss -ltnp. If you see a port with no owner, that is almost always the reason, not a bug.

lsof: when you think in files

lsof lists open files, and on Linux a socket is a file. -i :PORT selects internet sockets on that port:

terminal
$ lsof -i :8731
COMMAND    PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
python3 152425 nawawi    3u  IPv4 338786      0t0  TCP localhost:8731 (LISTEN)

By default lsof resolves names, so the address became localhost and well-known ports would show as http or mysql. On a busy server those lookups are slow. I usually add -n (no host names) and -P (no port names). I also restrict the output to listening TCP sockets, because -i :8731 on its own also matches every client connection to or from that port:

terminal
$ lsof -nP -iTCP:8731 -sTCP:LISTEN
COMMAND    PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
python3 152425 nawawi    3u  IPv4 338786      0t0  TCP 127.0.0.1:8731 (LISTEN)

The NODE column, 338786, is the socket inode mentioned above. For scripts, -t prints only PIDs, one per line, with no header:

terminal
$ lsof -t -i :8731
152425

The same permission rule applies, but lsof is quieter about it. Asking for a port owned by root prints nothing at all and exits with status 1, exactly as if nothing were listening:

terminal
$ lsof -i :3306; echo "rc=$?"
rc=1

That is the danger: ss showed port 3306 as listening, but lsof says nothing is there. Without root, an empty lsof result proves nothing.

fuser: the shortest answer

fuser, from the psmisc package, takes a port/protocol argument and prints the PIDs using it:

terminal
$ fuser 8731/tcp
8731/tcp:            152425
$ fuser -v 8731/tcp
                     USER        PID ACCESS COMMAND
8731/tcp:            nawawi    152425 F.... python3

The 8731/tcp: label goes to standard error and the PID to standard output, so pid=$(fuser 8731/tcp 2>/dev/null) captures just the number (with leading spaces). fuser -k 8731/tcp sends SIGKILL to everything using the port. It is useful, but check with -v first. Killing the wrong process with -9 skips its cleanup handlers.

Reading /proc/net/tcp when no tools are available

Containers and rescue systems often have none of these tools. The kernel still exposes the socket table in /proc/net/tcp (and /proc/net/tcp6 for IPv6), and you can read it with nothing more than grep. Addresses and ports are in hexadecimal, so first convert the port:

terminal
$ printf '%04X\n' 8731
221B
$ grep ':221B ' /proc/net/tcp
   0: 0100007F:221B 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 338786 1 0000000000000000 100 0 0 10 0

Reading the fields left to right: 0100007F:221B is the local address and port (the IPv4 address is stored in host byte order, so on x86 it reads backwards: 7F.00.00.01 is 127.0.0.1). The state 0A is LISTEN. 1000 is the UID of the owner. And 338786 is the socket inode. A short awk program turns the whole table into something readable (strtonum needs GNU awk):

bash
awk 'NR>1 && $4=="0A" {
    split($2, a, ":")
    printf "%d uid=%s inode=%s\n", strtonum("0x" a[2]), $8, $10
}' /proc/net/tcp
output
8731 uid=1000 inode=338786
8091 uid=1000 inode=220857
111 uid=0 inode=17271
3306 uid=0 inode=62463
443 uid=0 inode=68017
80 uid=0 inode=63479
8025 uid=0 inode=67793

Note that the UID is visible even for root's sockets, which already narrows the search. To get from the inode to a PID, do what ss -p does internally and search the file descriptor links:

terminal
$ find /proc/[0-9]*/fd -lname 'socket:\[338786\]' 2>/dev/null
/proc/152425/fd/3

PID 152425, the same answer as the other tools. Run the same search for port 3306's inode as a normal user and it finds nothing, for the same permission reason. As root it works.

Quick reference

TaskCommand
All listening TCP ports with ownerssudo ss -ltnp
One port, filteredsudo ss -ltnp 'sport = :8731'
UDP toosudo ss -lutnp
Listener on a port via lsofsudo lsof -nP -iTCP:8731 -sTCP:LISTEN
PID only, for scriptssudo lsof -t -i :8731
Shortestsudo fuser -v 8731/tcp
No tools installedgrep ':221B ' /proc/net/tcp

Once you have the PID, ps -o user,pid,args -p PID shows the full command line, which usually tells you which service or container it belongs to. If you want to wait in a script until a port is free, __pgrepf() shows a pure-Bash way of looking up processes by name.