ss -i shows you what netstat never could: TCP internals live
Published by RodHat

You know ss -tulnp. It’s the new netstat -tulnp and you use it to check if a port is open. Fine. That’s maybe fifteen percent of what ss can do.
The part people skip is ss -i, which dumps the tcp_info struct for every matched connection — congestion window size, round-trip time, retransmit count, send and receive buffer fill, the works. This is kernel-sourced, no instrumentation required, available on any Linux box running kernel 2.6+. You do not need a dashboard. You do not need your observability stack to be up. You need ss.
Why ss and not netstat
netstat reads /proc/net/tcp. That file has ports, addresses, socket state, and the inode — that’s it. The kernel puts it there for compatibility reasons, not because it’s useful.
ss reads from netlink, specifically NETLINK_INET_DIAG. The kernel fills a tcp_info struct for each connection and hands it over. This is the same struct the kernel uses internally to track TCP state. You’re reading the actual kernel data, not a scrubbed summary in a proc file.
Practically: ss is faster (no /proc parsing overhead at scale), has richer data, and supports server-side filtering so you’re not pulling the full socket table across netlink just to grep it.
The basic filter syntax
ss uses a filter language that looks like it was designed by someone who liked BPF. It was.
# Everything you know already
ss -tulnp
# Only established TCP
ss -t state established
# Only connections to a specific remote port
ss -t dst :443
# Only connections from a specific source
ss -t src 10.0.0.5
# Combine: established connections to port 5432
ss -t state established dst :5432
# All connections, local port range (useful for port exhaustion diagnosis)
ss -t sport gt :32768
# Connections with non-empty send queue (data stuck in flight)
ss -t '( snd_buf > 0 )'
State names: established, syn-sent, syn-recv, fin-wait-1, fin-wait-2, time-wait, closed, close-wait, last-ack, listen, closing. Also the meta-states all, connected, synchronized, bucket (TIME-WAIT + SYN-RECV), big (everything that isn’t a bucket state).
# Count connections by state — faster than netstat | awk
ss -t | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
# TIME_WAIT pile-up check
ss -t state time-wait | wc -l
ss -i: the actual useful part
ss -ti state established
Each connection gets a second line of TCP internal state. It looks like this:
ESTAB 0 0 10.0.1.5:22 10.0.1.100:53142
cubic wscale:7,7 rto:208 rtt:7.5/1.875 ato:40 mss:1448 pmtu:1500
rcvmss:1448 advmss:1448 cwnd:10 bytes_sent:4096 bytes_acked:4096
bytes_received:1024 segs_out:18 segs_in:12 data_segs_out:8
data_segs_in:4 send 15.4Mbps lastsnd:8 lastrcv:8 lastack:8
pacing_rate 18.4Mbps delivery_rate 15.4Mbps delivered:9
app_limited busy:24ms rcv_rtt:7.5 rcv_space:14480 rcv_ssthresh:64076
minrtt:6.875
What the important fields actually mean:
rto — retransmission timeout in milliseconds. This is how long the kernel waits before retransmitting a segment it hasn’t received an ACK for. High rto (>500ms on a LAN) means the kernel is seeing packet loss and backing off. Normal on a LAN is 200ms; on a WAN it scales with RTT.
rtt — smoothed round-trip time / RTT variance, in milliseconds. The first number is the SRTT (smoothed estimate), the second is the mean deviation. High variance means the path is jittery. This is the number you compare to your ping output — if ss rtt is 3x your ICMP RTT, something is wrong at the TCP layer.
cwnd — congestion window in segments (not bytes — multiply by mss for bytes). This is how many segments TCP is allowed to have in flight simultaneously. If cwnd is stuck at a small value (1, 2, 4) on a connection that should be moving data fast, the kernel thinks it’s seeing congestion and is throttling itself. On a LAN with no loss, cwnd should grow to fill the pipe quickly.
ssthresh — slow start threshold. When cwnd drops below ssthresh, TCP enters congestion avoidance mode. A very low ssthresh (4, 8) on a connection that should be fast is a sign of repeated packet loss events.
bytes_retrans / data_segs_out — retransmitted bytes and total data segments sent. The ratio tells you your retransmit rate. Anything above 0.1% on a LAN is worth investigating.
send — calculated transmit throughput based on current cwnd and RTT. This is what TCP thinks it’s achieving. Compare to your actual application throughput to see how much headroom exists.
rcv_space — receive buffer space offered to the sender. If this is small, the receiver is telling the sender to slow down because it can’t drain the buffer fast enough. Common cause: application reading from socket too slowly.
ss -K: kill a socket without touching the process
This one is underused. You can close a specific socket from outside the process with ss -K:
# Kill all established connections from a specific IP
ss -K dst 192.168.1.50
# Kill a specific connection by address and port
ss -K dst 192.168.1.50 dport = :443
The kernel closes the socket. The process gets an EOF (or a read error, depending on how it’s set up). The process does not die. This is useful for:
- Forcing a misbehaving connection to reconnect without restarting the service
- Clearing a stuck connection that the application refuses to drop
- Testing reconnection behavior under controlled conditions
Works on Linux 4.9+. Does not work on TCP connections in TIME_WAIT — those are already being closed by the kernel, they’ll expire on their own (tcp_timewait_len is 60 seconds, not configurable without a kernel parameter change).
Finding connections with elevated retransmits
# Show all established connections with retransmit info
ss -ti state established | grep retrans
Or the more surgical approach — filter on the second line field directly. ss doesn’t have a native filter for tcp_info fields, but since each connection is two lines:
ss -ti state established | paste - - | awk '/retrans/ {print}'
paste - - joins each pair of lines. Then you can awk on the combined output. If you have a specific connection behaving badly and want to watch it:
watch -n 1 'ss -ti dst :5432 state established'
Every second, live TCP state for all your PostgreSQL connections. No Prometheus required.
Checking buffer fill under load
The two numbers in the Recv-Q and Send-Q columns of ss -t are:
- Recv-Q: bytes received by the kernel but not yet read by the application
- Send-Q: bytes sent by the application but not yet acknowledged by the remote end
A persistently non-zero Recv-Q means the application is not draining the socket fast enough. The kernel will start telling the sender to slow down (window shrinkage), and throughput drops. A persistently non-zero Send-Q means the remote end isn’t ACKing — could be network loss, could be the remote end being slow, could be TCP flow control.
# Connections with non-empty receive queue
ss -t 'rcv_buf > 0'
# Or just watch the column
watch -n 1 'ss -tn state established | awk "NR==1 || \$2>0 || \$3>0"'
Comparing to what netstat could never show
netstat -s gives aggregate stats. Useful for trends, useless for per-connection debugging. When you have fifty established connections and one of them is misbehaving, netstat -s cannot tell you which one. ss -ti can.
netstat -tulnp shows listening sockets. ss -tulnp shows the same. Beyond that, netstat is done. ss goes deeper: congestion state, pacing rate, delivery rate, minRTT, ECN flags, timestamps, cubic vs BBR vs QUIC via cc field.
The TCP stack has always tracked this state. The tcp_info struct has existed since Linux 2.4. netstat just never bothered to surface it. ss does. Read from it.
If you want to understand why these fields exist and what the kernel is actually computing, the congestion control internals are in net/ipv4/tcp_input.c and net/ipv4/tcp_output.c in the kernel source. For the theory behind what cwnd, ssthresh, and RTO calculations mean, Stevens’ TCP/IP Illustrated, Volume 1 — Chapter 21 through 24 — is still the clearest treatment written. Kerrisk’s The Linux Programming Interface covers the socket options (TCP_INFO, SO_SNDBUF, SO_RCVBUF) that expose the same data programmatically from C.