Replacing netstat with ss: A Network Engineer's Diagnostic Guide
netstat is still on most Linux systems, but it is deprecated and absent from many minimal installs. Its replacement, ss (socket statistics), talks directly to the kernel via netlink rather than reading /proc/net/tcp, which makes it significantly faster on hosts with tens of thousands of sockets. More importantly, ss can expose per-socket TCP internals — retransmit counts, round-trip time, congestion window, send and receive buffer sizes — that netstat simply cannot provide.
This post covers ss from the basics through to the diagnostics that are actually useful when something is wrong on a live host.
Why ss is Faster
netstat reads /proc/net/tcp and /proc/net/tcp6, which are plain-text files the kernel generates on demand by walking its internal socket structures. On a host with 50,000 connections, generating and reading those files takes measurable time and memory.
ss uses the INET_DIAG netlink interface — a kernel subsystem specifically designed for socket diagnostics. Filtering happens inside the kernel before data is returned to user-space, so ss 'dst 10.0.0.1' only returns matching sockets; it never marshals the others. The difference is negligible on a lightly loaded server and dramatic on a busy one.
Common Flags
| Flag | Meaning |
|---|---|
-t | TCP sockets |
-u | UDP sockets |
-l | Listening sockets only |
-a | All sockets (listening + established) |
-p | Show owning process (PID and name) |
-n | No name resolution (raw IPs and port numbers) |
-e | Extended socket info (UID, inode, cookie) |
-i | Internal TCP info (RTT, cwnd, retransmits, etc.) |
-s | Summary statistics |
-4 / -6 | IPv4 or IPv6 only |
-o | Timer info (keepalive, retransmit, persist) |
Flags combine freely: ss -tnp is TCP + no name resolution + process info, which is the equivalent of netstat -tnp and the starting point for most diagnostics.
Basic Usage
What is listening?
# All listening TCP sockets, with process info
ss -tlnp
# All listening sockets (TCP + UDP)
ss -alnp
Sample output:
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1234,fd=3))
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=5678,fd=6))
LISTEN 0 128 127.0.0.1:3000 0.0.0.0:* users:(("node",pid=9012,fd=18))
Recv-Q on a listening socket is the number of connections in the accept backlog (completed handshakes not yet accept()-ed by the application). If this is non-zero and growing, the application is not draining its accept queue fast enough.
Established connections
# All established TCP connections
ss -tn state established
# With process names
ss -tnp state established
Summary
ss -s
Output:
Total: 312
TCP: 284 (estab 201, closed 47, orphaned 3, timewait 44)
Transport Total IP IPv6
RAW 0 0 0
UDP 8 6 2
TCP 237 190 47
INET 245 196 49
FRAG 0 0 0
A quick sanity check — look at timewait and orphaned counts before diving deeper.
The Filter Syntax
ss has a powerful filter language for selecting sockets by address, port, and state. Filters follow the flags:
ss [flags] [ FILTER ]
Filter by state
ss -t state established
ss -t state time-wait
ss -t state listen
ss -t state close-wait
ss -t state syn-sent
ss -t state syn-recv
# Exclude a state
ss -t exclude time-wait
# Multiple states
ss -t state established state close-wait
Filter by address and port
# All connections to a specific destination IP
ss -tn dst 10.0.0.1
# All connections from a specific source IP
ss -tn src 192.168.1.10
# Connections to a specific port
ss -tn dport = :443
# Connections from a specific local port
ss -tn sport = :22
# Connections to port 443 on a specific host
ss -tn dst 10.0.0.1:443
# Port range
ss -tn dport \>= :8000 dport \<= :8999
Note the backslash-escaping on >= and <= to prevent shell interpretation.
Combining filters
Filters chain with implicit AND:
# Established connections to 10.0.0.1 on port 443
ss -tn state established dst 10.0.0.1 dport = :443
TCP State Diagnostics
Understanding TCP states is essential for reading ss output correctly.
TIME_WAIT accumulation
ss -tn state time-wait | wc -l
TIME_WAIT is normal — the 2×MSL delay after a connection closes, preventing delayed packets from a previous connection confusing a new one. It becomes a problem when it exhausts the ephemeral port range:
# Current ephemeral port range (~28,000 ports on a default system)
cat /proc/sys/net/ipv4/ip_local_port_range
If the TIME_WAIT count approaches that range, outbound connections will start failing. Mitigations: enable SO_REUSEADDR, net.ipv4.tcp_tw_reuse (outbound only), or widen the port range.
CLOSE_WAIT — the application leak
ss -tnp state close-wait
CLOSE_WAIT means the remote end has sent FIN but the local application has not called close() yet. A persistent or growing CLOSE_WAIT count on a specific process almost always means a bug — the application is not closing file descriptors when the peer disconnects. Left unchecked, this exhausts file descriptor limits.
SYN_RECV — the SYN backlog
ss -tn state syn-recv
A high count of SYN_RECV sockets indicates either a SYN flood or a burst of incoming connections that the application is accepting slowly. Cross-reference with the accept backlog (Recv-Q on the LISTEN socket) to distinguish the two.
Established with retransmit timers
# Show timer info for established connections
ss -tno state established
The -o flag adds timer information:
ESTAB 0 0 192.168.1.10:22 192.168.1.1:54321 timer:(keepalive,1min52sec,0)
timer:(on,Xs,N) — retransmit timer, N retransmits already sent. A non-zero retransmit count on an established connection is worth investigating.
Per-Socket TCP Internals with -i
The -i flag exposes the TCP information that makes ss genuinely more powerful than netstat:
ss -tni state established
Sample output for one socket:
ESTAB 0 0 192.168.1.10:443 10.0.0.5:52341
cubic wscale:7,7 rto:204 rtt:3.5/1.2 ato:40 mss:1448 pmtu:1500
rcvmss:1448 advmss:1448 cwnd:10 bytes_sent:148234 bytes_retrans:0
bytes_acked:148234 bytes_received:2048 segs_out:105 segs_in:12
send 33.1Mbps lastsnd:8 lastrcv:8 lastack:4 pacing_rate 39.7Mbps
delivery_rate 33.1Mbps delivered:105 app_limited busy:72ms
rcv_space:14600 rcv_ssthresh:64088 minrtt:2.1 snd_wnd:65536
Key fields:
| Field | Meaning |
|---|---|
rtt | Smoothed RTT / variance in ms. rtt:3.5/1.2 = 3.5ms average, 1.2ms variance |
cwnd | Congestion window (segments). Low relative to bandwidth-delay product = throughput constrained |
bytes_retrans | Bytes retransmitted. Non-zero means packet loss on this flow |
send Xbps | Calculated max send throughput based on cwnd and RTT |
minrtt | Minimum observed RTT — closest to the true propagation delay |
pmtu | Path MTU in use. Less than interface MTU means PMTUD found a bottleneck |
mss | Maximum segment size negotiated |
rcv_space | Receive buffer space advertised to the peer |
Spotting retransmits on a live host
ss -tni state established | grep -B1 'bytes_retrans:[^0]'
Finding connections with high RTT
ss -tni state established \
| awk '/rtt:/ { match($0, /rtt:([0-9.]+)/, m); if (m[1]+0 > 100) print prev"\n"$0 } { prev=$0 }'
This surfaces any established connection with RTT above 100ms — useful for spotting unexpected long-haul or satellite-linked sessions.
Checking send buffer backpressure
ss -tn state established | awk '$3 > 0 { print }'
The third column is Send-Q — bytes in the kernel send buffer not yet acknowledged. Persistently non-zero means either a slow receiver or network congestion is backing up the sender.
Watching Connections in Real Time
ss is a snapshot tool, but watch bridges the gap:
# Refresh every second, show established count
watch -n1 'ss -tn state established | wc -l'
# Watch for new CLOSE_WAIT sockets on a specific process
watch -n2 'ss -tnp state close-wait | grep nginx'
For continuous logging:
while true; do
COUNT=$(ss -tn state time-wait | wc -l)
echo "$(date +%T) TIME_WAIT: $COUNT"
sleep 5
done
Practical Scenarios
What process is using port 8080?
ss -tlnp sport = :8080
How many connections does nginx have open?
ss -tnp | grep nginx | wc -l
Which remote IPs have the most connections to this host?
ss -tn state established \
| awk 'NR>1 {print $5}' \
| cut -d: -f1 \
| sort | uniq -c | sort -rn \
| head -20
Is the accept backlog filling up?
# Non-zero Recv-Q on LISTEN = connections waiting to be accept()ed
ss -tlnp | awk '$2 > 0'
Check PMTU in use for a specific connection
ss -tni dst 10.0.0.1 | grep pmtu
If pmtu is lower than the interface MTU (typically 1500), PMTUD has found a bottleneck on the path and reduced the MSS accordingly. The pmtud-sweeper tool can identify exactly which hop is responsible.
Find CLOSE_WAIT sockets by process, sorted by count
ss -tnp state close-wait \
| awk 'NR>1 {match($0, /\("([^"]+)"/, m); print m[1]}' \
| sort | uniq -c | sort -rn
All connections to a FortiGate management IP
ss -tnp dst 192.168.1.1
Comparing ss to netstat
| netstat | ss equivalent |
|---|---|
netstat -tlnp | ss -tlnp |
netstat -an | ss -an |
netstat -tnp | ss -tnp |
netstat -s | ss -s |
netstat -rn | ip route show (not ss — see the ip command post) |
What to Read Next
- Linux ip command — routing tables, policy routing rules, and ARP management; the tools that determine where the sockets
ssshows are actually sending their traffic - Linux tc — traffic shaping and QoS on the interfaces those connections traverse; combine with
ss -iRTT data to verify shaping is working as expected