ss: The Modern Socket Statistics Tool

Queue columns that tell you which end stopped working, filters that run in the kernel, and the TCP internals nothing else on the box will give you.

Terminal showing ss established sockets and ss -i TCP internals including rtt and congestion window

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         0

Eight 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:5201

Address 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:5201

Expressions 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:1

That 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:65280

Two 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:9000

The 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              * 0

Recipes worth keeping

ss -tulpn
What is listening, on which address, owned by what.
ss -tanp state syn-sent
Connections that are trying and failing. Firewall or routing.
ss -tanp state close-wait
Sockets the application forgot to close. Descriptor leak.
ss -tin dst 10.0.0.5
RTT, retransmits and congestion window to one peer.
ss -tan state time-wait | wc -l
Port exhaustion check on a busy proxy or load balancer.
watch -n1 "ss -tin dst 10.0.0.5"
Watch a connection's window and retransmits move in real time.

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 -tulpn answers "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 -i gives rtt, minrtt, cwnd, retransmitted bytes and delivery rate per connection. Nothing else on the box gives you that for free.
  • The gap between minrtt and rtt is queuing delay, and bytes_retrans against bytes_sent is per connection loss.
  • mss and pmtu in ss -i reflect what the connection believes, including a stale cached path MTU.
  • ss -K with a filter kills sockets without killing processes. Run the filter without -K first.

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.

Read next