A service accepts connections on a port only while a process holds a listening socket for it. ss, from iproute2, lists those sockets with the process behind each one. It replaces netstat, which Debian 12, Ubuntu 24.04, and RHEL 9 no longer install by default.
List the listening sockets
$ sudo ss -tlnpState Recv-Q Send-Q Local Address:Port Peer Address:Port ProcessLISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1204,fd=6),("nginx",pid=1203,fd=6))LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=812,fd=3))LISTEN 0 244 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=990,fd=6))LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=1204,fd=7),("nginx",pid=1203,fd=7))LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=812,fd=4))
-t shows TCP, -l only listening sockets, -n numbers instead of names, and -p the process. Run it with sudo: without root, the Process column stays empty for every socket that belongs to another user. For UDP, use -u instead of -t. UDP has no listening state, so its sockets show as UNCONN.
$ sudo ss -ulnpState Recv-Q Send-Q Local Address:Port Peer Address:Port ProcessUNCONN 0 0 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=402,fd=20))UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=402,fd=18))
For a listening TCP socket, Send-Q is the length of the queue of connections waiting to be accepted, and Recv-Q is how many are in it. A Recv-Q that stays near Send-Q means the program accepts connections more slowly than they arrive. The process name is cut to 15 characters, which is why systemd-resolved shows as systemd-resolve.
Read the address column
| Local address | Who can connect |
|---|---|
0.0.0.0:80 | Any IPv4 address of the machine, so anyone who can reach it |
[::]:80 | Any IPv6 address of the machine |
*:80 | Any address, IPv4 and IPv6, through one dual-stack socket |
127.0.0.1:5432 | Only programs on the same machine. [::1] is the same for IPv6. |
10.0.0.5:8080 | Only connections to that one address, here the private network |
127.0.0.53%lo:53 | That address, and only on the interface after the % sign |
In the first listing, PostgreSQL listens on 127.0.0.1 only, so other machines can’t reach it however the firewall is set. That’s its default; listen_addresses in postgresql.conf changes it.
Look up a single port
ss takes a filter after the options. Quote it, so the shell leaves it alone:
$ sudo ss -tlnp 'sport = :443'State Recv-Q Send-Q Local Address:Port Peer Address:Port ProcessLISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=1204,fd=8),("nginx",pid=1203,fd=8))
Two PIDs on one socket is normal for servers with a main process and workers: nginx’s main process opens the socket and its workers inherit it. lsof and fuser give the same answer, with the user each process runs as. lsof isn’t in a minimal install; it’s the lsof package on all three distributions.
$ sudo lsof -nP -iTCP:443 -sTCP:LISTENCOMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAMEnginx 1203 root 8u IPv4 21843 0t0 TCP *:443 (LISTEN)nginx 1204 www-data 8u IPv4 21843 0t0 TCP *:443 (LISTEN)$ sudo fuser 443/tcp443/tcp: 1203 1204
Find the service behind the process
A PID tells you the program, not which unit started it or how to restart it. Ask systemd:
$ ps -o unit= -p 1204nginx.service$ systemctl status 1204
systemctl status with a PID shows the unit that process belongs to, with its state and the last lines of its log. A process that isn’t in any service unit shows a session scope such as session-4.scope: someone started it from a shell, and no unit restarts it if it stops.
When the port is already in use
A service that can’t open its port fails at start with an error like this one in its journal:
$ journalctl -u nginx -n 5nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)$ sudo ss -tlnp 'sport = :80'State Recv-Q Send-Q Local Address:Port Peer Address:Port ProcessLISTEN 0 511 *:80 *:* users:(("apache2",pid=2290,fd=4),("apache2",pid=2288,fd=4))
Here Apache, installed as a dependency of something else, took port 80 first. Stop and disable the one you don’t want (sudo systemctl disable --now apache2), or move one of them to another port. Two programs can only share a port when each binds a different address, for example one on 10.0.0.5:80 and one on 127.0.0.1:80.
[Resolve]DNSStubListener=no
Ports held by systemd
With socket activation, systemd opens the port itself and starts the service on the first connection. ss then shows systemd with PID 1 as the owner, at least until the service has started. Ubuntu 24.04 runs OpenSSH this way, through ssh.socket.
$ sudo ss -tlnp 'sport = :22'State Recv-Q Send-Q Local Address:Port Peer Address:Port ProcessLISTEN 0 4096 *:22 *:* users:(("systemd",pid=1,fd=60))$ systemctl list-sockets
systemctl list-sockets lists every socket systemd holds, with the unit that holds it and the service it starts. To free such a port, stop and disable the .socket unit. Stopping only the service isn’t enough, because systemd starts it again on the next connection.
Listening isn’t the same as reachable
A socket on 0.0.0.0 accepts connections from anywhere the packets can arrive from, but a firewall on the machine or in front of it decides whether they arrive. To check that from the other side, see Test whether a TCP port is reachable.
Last checked on 24 October 2026 against Debian 12, Ubuntu 24.04, RHEL 9, iproute2 6.1+, and lsof 4.94+. If a step doesn’t match what you see, contact us with the command and what it printed.