Who is listening, who is connected and who owns the port: the modern replacement for netstat, with filters and what every column means
▶Prefer to watch? This video runs the guide's main commands on a real Ubuntu 24.04 machine, with on-screen captions and no voiceover. Click to play, or watch it on YouTube. It is part of the Networking and Firewalls playlist.
ss: sockets, listening ports and connections
ss -ltnp
ss (socket statistics) shows the network sockets on a machine: which programs are listening on which ports, which connections are open and in what state, and how many of each. It reads the kernel directly and replaces netstat, which is deprecated and often not installed any more. It is part of iproute2, so it is there on every modern Linux, next to ip.
The questions it answers: "is my service actually listening?", "on which address, only locally or for the whole network?", "what is using port 80?", "who is connected to me?", "why are there thousands of connections?"
Every example below ran for real on Ubuntu 24.04 (iproute2 6.1.0). To keep the output small and safe, the lab starts three tiny listeners on high ports: 18080 bound to 127.0.0.1 only, 18081 bound to all IPv4 addresses, and 18082 on IPv6 loopback, plus one open client connection to 18080. Commands that need root are shown without output.
The one command to remember: ss -ltnp
Each letter is a switch. Together they answer "what is listening":
-l listening sockets only (without it you get established connections, without listeners)
-t TCP (-u is UDP, -x is Unix sockets, -a is all states)
-n numbers: do not turn ports and addresses into names (faster, and shows the real port)
-p the process that owns each socket (needs sudo to see other users' processes)
-4 / -6 only IPv4 or only IPv6
$ ss -ltnp 'sport >= :18080 and sport <= :18082'
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 5 127.0.0.1:18080 0.0.0.0:* users:(("python3",pid=9342,fd=3))
LISTEN 0 5 0.0.0.0:18081 0.0.0.0:* users:(("python3",pid=9338,fd=3))
LISTEN 0 1 [::1]:18082 [::]:* users:(("python3",pid=9340,fd=3))
On a real server run sudo ss -ltnp, otherwise the Process column is empty for services running as other users (nginx, sshd, mysqld).
Reading the columns
State LISTEN (waiting for connections), ESTAB (connected), TIME-WAIT, SYN-SENT, SYN-RECV, CLOSE-WAIT and so on
Recv-Q for a LISTEN socket: connections waiting to be accepted by the program; for a connected socket: bytes received but not read yet
Send-Q for a LISTEN socket: the maximum backlog; for a connected socket: bytes sent but not acknowledged yet
Local Address:Port this machine's side. 0.0.0.0 or * means "all IPv4 addresses", [::] means "all IPv6 addresses"
Peer Address:Port the other side; for a listener it is a wildcard, because nobody is connected yet
Process users:(("name",pid=N,fd=N)): the program, its PID and the file descriptor holding the socket
A listener whose Recv-Q is above 0 and keeps growing, with Send-Q equal to the backlog, is a program that is not accepting connections fast enough (or is stuck). In the lab the Send-Q values 5 and 1 are the backlogs the test programs asked for.
Listening on 127.0.0.1 or on everything?
The Local Address column is the security question. 127.0.0.1:18080 can only be reached from this machine; 0.0.0.0:18081 can be reached from the network (if no firewall stops it); [::1] is the IPv6 loopback. The same program can be a private tool or an exposed service, depending on one line of its configuration:
$ ss -ltn 'sport = :18081'
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 5 0.0.0.0:18081 0.0.0.0:*
$ ss -4 -ltn 'sport >= :18080 and sport <= :18082'
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 5 127.0.0.1:18080 0.0.0.0:*
LISTEN 0 5 0.0.0.0:18081 0.0.0.0:*
$ ss -6 -ltn 'sport >= :18080 and sport <= :18082'
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 1 [::1]:18082 [::]:*
A database that should be local-only (MySQL, PostgreSQL, Redis) and shows 0.0.0.0:3306 or *:6379 is exposed: fix its bind-address setting and firewall it. A web service that "does not work from another machine" but shows 127.0.0.1:PORT is bound to loopback only. A service that listens on [::]:80 but not 0.0.0.0:80 may still accept IPv4 (dual-stack), so test with curl -4 and curl -6 before deciding.
Connections and states
Without -l, ss lists connected sockets. With one client holding a connection to port 18080 open, the server-side view is:
$ ss -tn state established '( sport = :18080 )'
Recv-Q Send-Q Local Address:Port Peer Address:Port Process
0 0 127.0.0.1:18080 127.0.0.1:42888
and the same connection seen from the client end:
$ ss -tn 'dport = :18080'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 0 127.0.0.1:42888 127.0.0.1:18080
Each connection appears twice on one machine (once for each end) when both ends are local. The client's port 42888 is an ephemeral port picked by the kernel.
The states you will see most often:
LISTEN waiting for connections
ESTAB an open connection
TIME-WAIT closed recently; the kernel keeps the entry for about 60 seconds to catch stray packets (many of these is normal on a busy web server)
CLOSE-WAIT the other side closed, but the local program has not closed its end: a lot of these means an application bug that leaks sockets
SYN-SENT this machine is trying to connect and has no answer yet (a firewall dropping packets, or a dead host)
SYN-RECV a connection is half-open; thousands may mean a SYN flood
FIN-WAIT-2 this side closed and waits for the other side to finish
Filtering: ports, addresses and states
ss has its own filter language, applied in the kernel, so it is fast even with hundreds of thousands of sockets. Put the expression in quotes so the shell leaves the parentheses and the angle brackets alone.
ss -tn 'sport = :443' connections or listeners on local port 443
ss -tn 'dport = :3306' connections going out to a MySQL server
ss -tn '( sport = :80 or sport = :443 )' either port
ss -tn 'sport >= :1024' a range (also <, <=, >, !=)
ss -tn dst 10.0.0.0/8 peer inside a subnet
ss -tn src 192.0.2.10 a specific local address
ss -tn state established '( dport = :22 )' a state plus a port
ss -tn state time-wait only one state (also: state listening, state syn-sent, state close-wait, state all)
ss -tn -o state established with timers
States work as a word (state established) or a set: state connected (every state except LISTEN and CLOSE), state synchronized, state bucket, state big.
Filtering on one port gives a one-line answer. With -H (no header) and grep, the owning PID of the port 18080 listener comes out on its own:
$ ss -ltnpH 'sport = :18080' | grep -o 'pid=[0-9]*'
pid=9342
-H removes the header line, which is what you want in scripts.
Who owns the port: processes with -p
The classic "address already in use" hunt: which program has the port?
$ sudo ss -ltnp 'sport = :80'
(prints the program, its PID and the file descriptor; the listing above shows the format)
Then ps -fp PID to see how it was started, or systemctl status PID to find the service it belongs to. Without sudo, ss only shows processes you own, so an empty Process column does not mean "nobody": it means "not yours".
To see every socket of one process: ss -tnp | grep 'pid=1234', or lsof -i -a -p 1234 (see the lsof page).
Summary and counting
ss -s prints the totals first, a quick view of how busy the machine is:
$ ss -s
Total: 1002
TCP: 32 (estab 11, closed 10, orphaned 0, timewait 5)
Transport Total IP IPv6
RAW 1 0 1
UDP 7 5 2
TCP 22 18 4
INET 30 23 7
FRAG 0 0 0
To count connections per state, the best one-liner to know:
$ ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
11 LISTEN
11 ESTAB
5 TIME-WAIT
A server with tens of thousands of SYN-RECV or CLOSE-WAIT entries has a problem that this shows at once. To count connections per remote address (who is hammering the server):
$ ss -tn state established | awk 'NR>1 {split($4,a,":"); print a[1]}' | sort | uniq -c | sort -rn | head
Timers and TCP details
-o shows the timer attached to each socket (retransmissions, keep-alive, time-wait expiry); -i adds the internal TCP numbers the kernel keeps for each connection:
$ ss -tnoi state established '( sport = :18080 )'
Recv-Q Send-Q Local Address:Port Peer Address:Port Process
0 0 127.0.0.1:18080 127.0.0.1:42888
ts sack cubic wscale:10,10 rto:200 rtt:0.043/0.021 mss:32768 pmtu:65535 rcvmss:536 advmss:65483 cwnd:10 segs_in:2 ...
rtt is the measured round-trip time in milliseconds (average/variance) and rto the retransmission timeout; cwnd is the congestion window; the congestion algorithm (cubic here) is named. On a real link, a large rtt, a growing retrans count or a tiny cwnd on one connection explains "this one download is slow". -e adds the owning user ID and inode; -m shows socket memory.
ss can also end connections, if the kernel supports it and you are root: sudo ss -K dst 203.0.113.9 dport = 443 (a quick way to cut a stuck connection without killing the program). Not every kernel is built with this, and without root it fails.
UDP and Unix sockets
ss -uln listening UDP sockets (DNS, DHCP, NTP, syslog, mDNS)
ss -uan all UDP sockets
ss -lnx listening Unix sockets (docker.sock, mysqld.sock, systemd's own)
ss -xlp who owns them
ss -S SCTP sockets
UDP has no connections, so you will see the state UNCONN for a socket that is just bound, and ESTAB only for UDP sockets that were connect()-ed. A UDP "listener" is simply a bound socket with an empty peer.
netstat and lsof equivalents
netstat -tlnp ss -tlnp
netstat -tunap ss -tunap
netstat -s nstat or ss -s (netstat -s also has the protocol counters: use nstat -az)
netstat -rn ip route
netstat -i ip -s link
lsof -iTCP -sTCP:LISTEN -nP sudo ss -ltnp
lsof -i :80 sudo ss -ltnp 'sport = :80'
fuser 80/tcp sudo ss -ltnp 'sport = :80'
ss is faster than netstat on busy machines, because netstat parses text files in /proc and ss asks the kernel through a netlink socket.
Recipes
sudo ss -ltnp every listening TCP service and its program
sudo ss -ltnp | grep -E '0\.0\.0\.0|\*:|\[::\]' what is exposed beyond localhost (a quick audit)
sudo ss -ltnp 'sport = :8080' who has that port?
ss -tan | awk 'NR>1{print $1}' | sort | uniq -c connection count by state
ss -tn state established '( dport = :443 )' | wc -l outbound HTTPS connections
ss -tn state syn-sent connections this machine cannot complete (firewall or dead host)
ss -tn state close-wait | wc -l leaked sockets
watch -n1 'ss -s' totals, refreshed every second
ss -ltn | awk 'NR>1{print $4}' | sort local addresses of all listeners, for a diff before and after a change
ss -tno state established '( sport = :22 )' SSH sessions and their keep-alive timers
Common problems
Process column is empty you are not root, or the process belongs to another user: run sudo ss -ltnp
The service is up but nothing shows on its port you filtered with -4 and it listens on IPv6 only (or the other way round): run ss -ltn without -4 or -6
"Address already in use" when starting a service another program already listens there: sudo ss -ltnp 'sport = :PORT', stop that one or change the port; a socket in TIME-WAIT may block a fast restart for a while unless the program sets SO_REUSEADDR
Listener shows 127.0.0.1 and other machines cannot connect it is bound to loopback only: change its bind/listen address (and then open the firewall)
Port shows LISTEN but connections hang the firewall drops the packets: from the client, ss -tn state syn-sent shows the attempt; check ufw status or nft list ruleset on the server
Thousands of TIME-WAIT normal for a busy client or proxy; reuse connections (keep-alive) rather than tuning kernel settings first
Thousands of CLOSE-WAIT that never go away the application never closes sockets it has been told are closed: a bug in the application, restart is the short-term fix
ss: command not found install iproute2 (apt install iproute2 or dnf install iproute)
"RTNETLINK answers: Invalid argument" with -K not root, or the kernel lacks socket destroy support
Output looks different between machines old iproute2 versions print columns differently: use -H and awk for scripts, or ss -O for one-line output
Tips
sudo ss -ltnp is the first command to run when "the port is not working". Read the Local Address column before anything else.
127.0.0.1 means private, 0.0.0.0 and [::] mean public. Check this after every install of a database or cache.
Quote filter expressions: ss -tn 'sport = :443'.
Use -H and -n in scripts: no header, no name lookups, no surprises.
Count states with awk, uniq and sort before reaching for a monitoring tool; the answer is usually in the first number that looks wrong.
Combine ss with lsof and ps: ss tells you what is bound, lsof what a process has open, ps how the process was started.