🔌 ss Guide

Who is listening, who is connected and who owns the port: the modern replacement for netstat, with filters and what every column means

Video: ss (sysadmin video)
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.