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

FlagMeaning
-tTCP sockets
-uUDP sockets
-lListening sockets only
-aAll sockets (listening + established)
-pShow owning process (PID and name)
-nNo name resolution (raw IPs and port numbers)
-eExtended socket info (UID, inode, cookie)
-iInternal TCP info (RTT, cwnd, retransmits, etc.)
-sSummary statistics
-4 / -6IPv4 or IPv6 only
-oTimer 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:

FieldMeaning
rttSmoothed RTT / variance in ms. rtt:3.5/1.2 = 3.5ms average, 1.2ms variance
cwndCongestion window (segments). Low relative to bandwidth-delay product = throughput constrained
bytes_retransBytes retransmitted. Non-zero means packet loss on this flow
send XbpsCalculated max send throughput based on cwnd and RTT
minrttMinimum observed RTT — closest to the true propagation delay
pmtuPath MTU in use. Less than interface MTU means PMTUD found a bottleneck
mssMaximum segment size negotiated
rcv_spaceReceive 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

netstatss equivalent
netstat -tlnpss -tlnp
netstat -anss -an
netstat -tnpss -tnp
netstat -sss -s
netstat -rnip route show (not ss — see the ip command post)
  • Linux ip command — routing tables, policy routing rules, and ARP management; the tools that determine where the sockets ss shows are actually sending their traffic
  • Linux tc — traffic shaping and QoS on the interfaces those connections traverse; combine with ss -i RTT data to verify shaping is working as expected