Most people use exactly one ss command: ss -tulpn, to find out what is listening. That is a shame, because socket state is where a surprising number of network arguments actually get settled. Is the application listening at all, or only on loopback? Is the connection established but stalled? Is data queued in the kernel because the far end stopped reading? What round trip time and congestion window did TCP settle on? All of that is one command away, and none of it is visible from a packet capture without a lot more work.
This article works through ss against a host with real traffic on it: four parallel iperf3 streams and two netcat sessions to a server four hops away in a Cisco Modeling Labs topology, plus local listeners. Every capture is genuine output from a Debian 13 host, and this article is part of the Linux networking commands cluster.
The grammar
ss takes flags that choose what to show and how much detail, then an optional filter expression. Everything else follows from that:
-tTCP sockets
-uUDP sockets
-xUnix domain sockets
-aAll sockets, not just connected ones. Without it, listeners are hidden.
-lListening sockets only
-nNumeric. Do not translate 22 into "ssh" or resolve addresses.
-pShow the owning process. Needs root to see other users' sockets.
-iTCP internals: rtt, congestion window, retransmits, MSS
-mSocket memory usage
-sSummary counts by protocol and state
Start from the top, with the ten thousand foot view:
j@llmbits:~$ ss -s
Total: 495
TCP: 15 (estab 8, closed 0, orphaned 0, timewait 0)
Transport Total IP IPv6
RAW 1 0 1
UDP 5 3 2
TCP 15 12 3
INET 21 15 6
FRAG 0 0 0Eight established TCP connections, no time-wait, nothing orphaned. This is the first thing to run on a box that "feels wrong," because a socket table with tens of thousands of time-wait entries or a pile of orphans is a very different problem from one with fifteen sockets on it.
ss -tulpn, and what it is really telling you
j@llmbits:~$ sudo ss -tulpn
Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
udp UNCONN 0 0 0.0.0.0:57029 0.0.0.0:* users:(("avahi-daemon",pid=896,fd=14))
udp UNCONN 0 0 0.0.0.0:5353 0.0.0.0:* users:(("avahi-daemon",pid=896,fd=12))
udp UNCONN 0 0 [::]:43891 [::]:* users:(("avahi-daemon",pid=896,fd=15))
udp UNCONN 0 0 [::]:5353 [::]:* users:(("avahi-daemon",pid=896,fd=13))
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1272,fd=6))
tcp LISTEN 0 4096 127.0.0.1:631 0.0.0.0:* users:(("cupsd",pid=8139,fd=7))
tcp LISTEN 0 10 0.0.0.0:7000 0.0.0.0:* users:(("nc",pid=10374,fd=4))
tcp LISTEN 0 5 0.0.0.0:8080 0.0.0.0:* users:(("python3",pid=10373,fd=3))
tcp LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=1272,fd=7))
tcp LISTEN 0 4096 [::1]:631 [::]:* users:(("cupsd",pid=8139,fd=6))
tcp LISTEN 0 10 [::]:7000 [::]:* users:(("nc",pid=10374,fd=3))The single most valuable column here is Local Address, because it answers the question that generates so many "the firewall must be blocking it" tickets. cupsd is bound to 127.0.0.1:631. It is not reachable from another machine and no firewall change will ever make it so. The python server on 0.0.0.0:8080 is bound to every IPv4 address and is reachable from anywhere the network allows.
The second most valuable is Send-Q on a listening socket, which is not a queue at all. On a LISTEN row, Send-Q is the accept backlog: 128 for sshd, 4096 for cupsd, 5 for the python server. Recv-Q on a LISTEN row is the number of connections currently sitting in that backlog waiting to be accepted. If Recv-Q on a listener is persistently non zero, the application is not calling accept() fast enough and you have found a real bottleneck.
Note also that 0.0.0.0 and [::] entries appear separately. Port 22 is listening on both stacks. Port 8080 is IPv4 only, which is worth knowing before somebody spends an afternoon wondering why the AAAA record does not work.
Established connections
With four iperf3 streams and two netcat sessions running to a server four hops away:
j@llmbits:~$ ss -tan
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 4096 127.0.0.1:631 0.0.0.0:*
LISTEN 0 10 0.0.0.0:7000 0.0.0.0:*
LISTEN 0 5 0.0.0.0:8080 0.0.0.0:*
ESTAB 0 296560 10.77.0.100:52788 10.77.3.10:5201
ESTAB 0 0 10.77.0.100:47614 10.77.3.10:9001
ESTAB 0 284428 10.77.0.100:52766 10.77.3.10:5201
ESTAB 0 0 10.77.0.100:58176 10.77.3.10:9000
ESTAB 0 175240 10.77.0.100:52776 10.77.3.10:5201
ESTAB 0 0 192.168.88.156:22 192.168.88.125:58434
ESTAB 0 248032 10.77.0.100:52790 10.77.3.10:5201
ESTAB 0 0 10.77.0.100:52758 10.77.3.10:5201
LISTEN 0 128 [::]:22 [::]:*On an ESTAB row the queue columns mean what you would expect, and they mean a great deal.
Send-Q is bytes the application has handed to the kernel that have not been acknowledged by the far end. The four iperf3 data streams are sitting at 175 KB to 296 KB, which is exactly right: iperf3 is pushing as hard as it can and the kernel is holding a window's worth of unacknowledged data. The netcat sessions and the fifth iperf3 socket (its control channel) show zero, because they are idle.
Recv-Q is bytes that have arrived and the application has not read. A non zero Recv-Q that stays non zero means the local application is not keeping up. That is the signature of a hung or blocked process, and it is the fastest way to distinguish "the network is slow" from "the program stopped reading."
A persistently large Send-Q with no progress means the opposite: the far end is not acknowledging, either because it is overwhelmed or because the path is broken in a way that ping did not catch.
Filters are the point
ss filters in the kernel, so filtering with ss is both faster and more precise than piping into grep. The state keyword comes first, then an optional parenthesized expression:
j@llmbits:~$ ss -tan state established '( dport = :5201 )'
Recv-Q Send-Q Local Address:Port Peer Address:Port
0 260164 10.77.0.100:52788 10.77.3.10:5201
0 212984 10.77.0.100:52766 10.77.3.10:5201
0 235900 10.77.0.100:52776 10.77.3.10:5201
0 203548 10.77.0.100:52790 10.77.3.10:5201
0 0 10.77.0.100:52758 10.77.3.10:5201Address filters take a prefix, which grep cannot do correctly at all:
j@llmbits:~$ ss -tan dst 10.77.3.0/24
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 191416 10.77.0.100:52788 10.77.3.10:5201
ESTAB 0 0 10.77.0.100:47614 10.77.3.10:9001
ESTAB 0 157716 10.77.0.100:52766 10.77.3.10:5201
ESTAB 0 0 10.77.0.100:58176 10.77.3.10:9000
ESTAB 0 138844 10.77.0.100:52776 10.77.3.10:5201
ESTAB 0 148280 10.77.0.100:52790 10.77.3.10:5201
ESTAB 0 0 10.77.0.100:52758 10.77.3.10:5201Expressions combine with and, or, not, and ports support ranges:
j@llmbits:~$ sudo ss -tanp4 '( dport >= :9000 and dport <= :9001 )'
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
ESTAB 0 0 10.77.0.100:47614 10.77.3.10:9001 users:(("nc",pid=10382,fd=3))
ESTAB 0 0 10.77.0.100:58176 10.77.3.10:9000 users:(("nc",pid=10378,fd=3))The keywords worth knowing: dst and src for addresses, dport and sport for ports, and the state names established, listening, time-wait, syn-sent, syn-recv, fin-wait-1, close-wait, plus the shorthand groups connected, synchronized, bucket and big.
Two of those states are worth troubleshooting habits of their own. A pile of syn-sent means your host is trying to connect and getting nothing back, which is a network or firewall problem. A pile of close-wait means the far end closed and your application never called close(), which is an application bug and will eventually exhaust file descriptors.
Tying sockets to processes
j@llmbits:~$ sudo ss -tanp dst 10.77.3.10
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
ESTAB 0 227812 10.77.0.100:52788 10.77.3.10:5201 users:(("iperf3",pid=10379,fd=9))
ESTAB 0 0 10.77.0.100:47614 10.77.3.10:9001 users:(("nc",pid=10382,fd=3))
ESTAB 0 215680 10.77.0.100:52766 10.77.3.10:5201 users:(("iperf3",pid=10379,fd=5))
ESTAB 0 0 10.77.0.100:58176 10.77.3.10:9000 users:(("nc",pid=10378,fd=3))
ESTAB 0 225116 10.77.0.100:52776 10.77.3.10:5201 users:(("iperf3",pid=10379,fd=7))
ESTAB 0 218376 10.77.0.100:52790 10.77.3.10:5201 users:(("iperf3",pid=10379,fd=11))
ESTAB 0 0 10.77.0.100:52758 10.77.3.10:5201 users:(("iperf3",pid=10379,fd=4))All five iperf3 sockets belong to one process, pid 10379, on file descriptors 4, 5, 7, 9 and 11. That is what -P 4 looks like from the kernel's side: one control connection on fd 4 and four data streams. The file descriptor number is genuinely useful when a process has hundreds of sockets and you want to correlate with strace or lsof output.
Without sudo you will see your own processes and blank entries for everyone else's. It is a permissions boundary, not a bug.
ss -i: the numbers nobody else will give you
This is the flag that earns ss its place, and it has no equivalent in netstat at all:
j@llmbits:~$ ss -tin dst 10.77.3.10 | head -6
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 188720 10.77.0.100:52788 10.77.3.10:5201
cubic wscale:11,7 rto:208 rtt:7.952/1.148 mss:1348 pmtu:1400 rcvmss:536 advmss:1348 cwnd:6 ssthresh:4 bytes_sent:10759773 bytes_retrans:191416 bytes_acked:10560270 segs_out:7985 segs_in:4419 data_segs_out:7983 send 8136821bps lastsnd:4 lastrcv:8688 lastack:4 pacing_rate 9763264bps delivery_rate 7301280bps delivered:7836 busy:8676ms unacked:6 retrans:0/142 rcv_space:13480 rcv_ssthresh:64188 notsent:180632 minrtt:2.721 snd_wnd:790528 rcv_wnd:65280 rehash:1That is a lot of text, so here are the fields that pay for the reading:
cubicThe congestion control algorithm in use for this socket. BBR here would change how you read everything else.
rtt:7.952/1.148Smoothed round trip time and its variance, in milliseconds, measured by TCP itself. This is a better latency figure than ping, because it is the latency the application is actually experiencing.
minrtt:2.721The lowest RTT ever seen on this connection. The gap between minrtt and rtt is queuing delay, which is bufferbloat measured for free.
mss and pmtuSegment size and the path MTU the stack believes in. See below, because this one has a story.
cwnd / ssthreshCongestion window in segments and the slow start threshold. A cwnd of 6 on a fast path means congestion control has clamped down, usually because of loss.
bytes_retrans191 KB retransmitted out of 10.7 MB sent, or roughly 1.8 percent. Per connection loss, without a packet capture.
delivery_rate7.3 Mbps here. What this connection is actually achieving, as opposed to what the link could do.
notsentBytes in the send buffer TCP has not put on the wire yet, because the window will not allow it. The application is ahead of the network.
Now the story in mss:1348 pmtu:1400. Nothing on this path had a 1400 byte MTU when these connections were made. It had one twenty minutes earlier, while an MTU discovery experiment was running, and the cached path MTU has a ten minute lifetime. TCP took the number it found, subtracted 40 bytes of IPv4 and TCP header, and negotiated an MSS of 1348 for every one of these sockets. ss -i shows you what your connections believe, which is not always what the network currently is. That distinction has explained more than one "but I fixed the MTU" conversation.
The idle netcat socket makes an instructive contrast:
ESTAB 0 0 10.77.0.100:47614 10.77.3.10:9001
cubic wscale:11,7 rto:240 rtt:38.183/19.091 mss:1348 pmtu:1400 rcvmss:536 advmss:1348 cwnd:10 bytes_acked:1 segs_out:2 segs_in:1 send 2824294bps lastsnd:8536 lastrcv:8536 lastack:8536 pacing_rate 5648584bps delivered:1 app_limited rcv_space:13480 rcv_ssthresh:64188 minrtt:38.183 snd_wnd:65160 rcv_wnd:65280Two segments out, one in, and app_limited, meaning throughput is bounded by the application having nothing to send rather than by the network. The rtt of 38 ms is derived from the single handshake sample and is not worth anything. lastsnd:8536 says nothing has moved in 8.5 seconds. That is what an idle connection looks like, and being able to recognize it stops you from investigating a healthy socket.
Socket memory
j@llmbits:~$ ss -tanm dst 10.77.3.10 | head -8
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 269600 10.77.0.100:52788 10.77.3.10:5201
skmem:(r0,rb131072,t0,tb261120,f288,w278240,o0,bl0,d0)
ESTAB 0 0 10.77.0.100:47614 10.77.3.10:9001
skmem:(r0,rb131072,t0,tb87040,f0,w0,o0,bl0,d0)
ESTAB 0 279036 10.77.0.100:52766 10.77.3.10:5201
skmem:(r0,rb131072,t0,tb243712,f1348,w223932,o0,bl0,d0)rb and tb are the receive and send buffer sizes, w is memory committed to send queue data, and d is packets dropped by this socket. That last one matters: a non zero d is the kernel telling you it discarded data because the socket buffer was full, which is a host problem and will never appear as loss on the network.
Notice the buffers are not equal. The busy iperf3 socket has grown its send buffer to 261 KB while the idle netcat socket sits at 87 KB. That is TCP autotuning, and it is why a fixed buffer size in a tuning guide is usually worse than the default.
Killing a socket
ss -K destroys sockets matching a filter, which is the polite way to clear a stuck connection without killing the process that owns it:
j@llmbits:~$ sudo ss -K dst 10.77.3.10 dport = 9001
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp ESTAB 0 0 10.77.0.100:47614 10.77.3.10:9001
j@llmbits:~$ ss -tan state established '( dport = :9000 or dport = :9001 )'
Recv-Q Send-Q Local Address:Port Peer Address:Port
0 0 10.77.0.100:58176 10.77.3.10:9000The 9001 socket is gone and the 9000 socket, which is the same program pattern to the same host, is untouched. It prints what it killed, which is the behavior you want from a destructive command. This needs root and CONFIG_INET_DIAG_DESTROY in the kernel, which every mainstream distribution ships. Get the filter wrong here and you will drop production connections, so run it without -K first and read the list.
Unix and UDP sockets
UDP has no connection state, so most rows read UNCONN. A UDP socket that has had connect() called on it does show as ESTAB, which is how the DHCP client appears here:
j@llmbits:~$ ss -uan
State Recv-Q Send-Q Local Address:Port Peer Address:Port
UNCONN 0 0 0.0.0.0:57029 0.0.0.0:*
ESTAB 0 0 192.168.88.156%ens192:68 192.168.88.1:67
UNCONN 0 0 0.0.0.0:5353 0.0.0.0:*
UNCONN 0 0 [::]:43891 [::]:*
UNCONN 0 0 [::]:5353 [::]:*The %ens192 suffix names the interface the socket is bound to, which is worth knowing on a multihomed host.
Unix domain sockets are where local service problems live. Half of what looks like a network problem on a modern Linux box is actually a process failing to reach /run/dbus/system_bus_socket or a similar path:
j@llmbits:~$ ss -xl | head -6
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
u_str LISTEN 0 4096 /run/dbus/system_bus_socket 848 * 0
u_str LISTEN 0 4096 /run/ssh-unix-local/socket 851 * 0
u_str LISTEN 0 4096 /run/systemd/io.systemd.Hostname 853 * 0
u_str LISTEN 0 128 /tmp/ssh-BjUKybAeDsun/agent.1416 9189 * 0Recipes worth keeping
FAQ
Why use ss instead of netstat?
ss reads socket state through the netlink sock_diag interface rather than parsing text out of /proc/net, which makes it faster on hosts with many sockets, and it exposes per socket TCP internals that netstat has no way to reach. The full comparison with timings is in netstat vs ss.
What does Recv-Q mean on a listening socket?
Connections completed by the kernel and waiting for the application to accept(). Send-Q on that same row is the maximum backlog. Recv-Q approaching Send-Q on a listener means clients are about to start seeing connection refused or timeouts.
Why is the Process column empty?
You are not root, so you can only see your own processes' sockets. Re run under sudo.
Should I trust ss rtt over ping?
For diagnosing an application, yes. ss reports the round trip time TCP measured on the actual data flow, on the actual path, with the actual packet sizes. Ping measures ICMP echo handling, which routers and hosts often treat with a different priority. When ping and ss disagree, ss is describing what your users experience.
Is a large number of time-wait sockets a problem?
Usually not. Time-wait is a normal 60 second state that protects against delayed duplicates. It becomes a problem only when a host makes so many short outbound connections that it exhausts its ephemeral port range, which you would see as connection failures rather than as the count itself.
Key takeaways
ss -tulpnanswers "is it listening and on what address." A service bound to 127.0.0.1 will never be reachable remotely, no matter what the firewall says.- On a LISTEN row, Send-Q is the accept backlog and Recv-Q is the pending queue depth. On an ESTAB row they are unacknowledged and unread bytes.
- Persistent Recv-Q means the local application stopped reading. Persistent Send-Q means the far end stopped acknowledging.
- Filters run in the kernel.
ss -tan state established '( dport = :443 )'beats piping to grep on both speed and correctness. ss -igives rtt, minrtt, cwnd, retransmitted bytes and delivery rate per connection. Nothing else on the box gives you that for free.- The gap between
minrttandrttis queuing delay, andbytes_retransagainstbytes_sentis per connection loss. mssandpmtuinss -ireflect what the connection believes, including a stale cached path MTU.ss -Kwith a filter kills sockets without killing processes. Run the filter without-Kfirst.
For why ss replaced netstat, with wall clock timings on a host holding ten thousand sockets, read netstat vs ss. The path side of the picture is covered in traceroute and mtr, and the MTU numbers that show up in ss -i come from path MTU discovery. Everything in this series is indexed on the Linux networking commands guide.