# ss: socket statistics instead of deprecated netstat

LLMS index: [llms.txt](/en/llms.txt)

---

When `netstat` hangs on a server with tens of thousands of connections, it's time to switch to `ss`. Part of the `iproute2` package, `ss` queries the kernel directly via netlink instead of parsing `/proc/net/*`. The result is instant output with minimal overhead.

## Why switch from netstat

`netstat` from `net-tools` relies on a deprecated approach: it reads from `/proc/net/tcp`, `/proc/net/unix` and converts numeric IDs to symbolic names. On a server with active connections, this takes seconds and spikes CPU usage.

`ss` communicates with the kernel through a netlink socket. One call, structured data returned. For 10,000 connections, the difference is 0.02 seconds versus 3–5 seconds.

> [!NOTE]
> `netstat` is officially marked as deprecated in most distributions. `iproute2` is the current standard for network management in Linux.

Separate installation is rarely needed: `ss` ships with `iproute2`, which is present in every Linux by default.

## Basic flags: the -tulnp equivalent

No need to relearn everything — flags are similar, just with more flexible ordering:

```bash
# Listening ports, show processes
ss -tlnp
```

| Flag | What it shows |
|------|---------------|
| `-t` | TCP sockets |
| `-u` | UDP sockets |
| `-l` | Listening sockets only |
| `-n` | Numeric addresses and ports (no DNS) |
| `-p` | Process owner (PID, name) |
| `-a` | All sockets (not just listening) |
| `-e` | Extended info (uid, inode) |
| `-o` | Timer information |

Flag order doesn't matter — `-tlnp` and `-ltnp` are the same. `-p` needs root to show processes owned by other users.

Output differs structurally from `netstat`:

```
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      127.0.0.1:6379       0.0.0.0:*          users:(("redis-server",pid=5678,fd=6))
```

Key columns:

| Column | Meaning |
|--------|---------|
| Recv-Q | Bytes in receive buffer, not yet read by application |
| Send-Q | Bytes in send buffer, not acknowledged by peer |
| Local Address:Port | Local end of the connection |
| Peer Address:Port | Remote end |

> [!TIP]
> Non-zero Recv-Q or Send-Q on an established connection signals a problem. The application isn't keeping up with reads, or the network is congested.

Full set of basic filters:

```bash
ss -t       # TCP only
ss -u       # UDP only
ss -w       # raw sockets
ss -x       # Unix sockets
ss -a       # all (listening and established)
ss -l       # listening only
```

## Filtering by state and port

This is where `ss` beats `netstat`. Filters are native, not piped through `grep`:

```bash
# Established connections only
ss -t state established

# Active connections (not listening)
ss -t state connected

# TIME_WAIT — the classic use case
ss -t state time-wait

# Everything except listening
ss -t state connected -s
```

State combinations:

```bash
# ESTABLISHED only, with process info
ss -t state established -p

# SYN-SENT, SYN-RECV — handshake issues
ss -t state syn-sent

# FIN-WAIT-1, FIN-WAIT-2
ss -t state fin-wait1,fin-wait2

# CLOSE-WAIT — connection hanging, waiting to close
ss -t state close-wait

# Grouped states
ss -tan 'state established or state time-wait'
```

Port filter — one of the most common:

```bash
# Who is listening on 443
ss -tlnp 'sport = :443'

# Who is connected to 5432 (PostgreSQL)
ss -tp 'dport = :5432'

# All connections to any port 80 or 443
ss -t 'sport = :80 or dport = :80 or sport = :443 or dport = :443'
```

> [!WARNING]
> The `sport` and `dport` filters work with numeric values. For ranges, use `>=` and `<=`: `'dport >= 3000 and dport <= 4000'`.

Address filter:

```bash
# Connections from a specific IP
ss -tp 'src 192.168.1.100'

# Outbound connections (not from local network)
ss -tp 'not src 192.168.0.0/16'
```

## Extended output: -e, -i, -s

For queue diagnostics and statistics:

```bash
# Detailed information (extended)
ss -teln

# Adds:
# - uid (user)
# - inode
# - timers (for keepalive, TIME_WAIT)
# - timeout
```

Output with `-e` for an established connection:

```
ESTAB 0 0 10.0.0.5:22 10.0.0.100:52431 users:(("sshd",pid=1820,fd=3)) uid=1000 ino=35234 sk=0xffff88003a2c8000 <->
```

**Interface information (`-i`):**

```bash
ss -ti 'dst 10.0.0.1'
```

```
ESTAB 0 0 10.0.0.5:22 10.0.0.100:52431
         ts sack hbrs pmtu cwnd rtt rttvar unacked
         wscale:7,7 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10
         send 0.4Mbps rcv_space:43690
```

Key metrics:

| Metric | Description |
|--------|-------------|
| cwnd | Congestion window |
| rtt | Round-trip time |
| pmtu | Path MTU |
| rcv_space | Receive buffer size |
| send | Current send rate |

**State statistics (`-s`):**

```bash
ss -s
```

```
Total: 124 (kernel 128)
TCP:   45 (estab 38, closed 2, orphaned 0, synrecv 0, timewait 2)

Transport Total     IP          IPv6
*         128       -           -
RAW       0         0           0
UDP       12        8           4
TCP       43        38          5
INET      55        46          9
FRAG      0         0           0
```

> [!NOTE]
> The `timewait 2` in `ss -s` output is a quick way to assess connection buildup. If the number grows each time you run it — something isn't closing connections properly.

## Common use cases

**Which process is listening on a port and which interface:**

```bash
ss -tlnp 'sport = :3306'
```

```
State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
LISTEN  0       128     0.0.0.0:3306        0.0.0.0:*         users:(("mysqld",pid=2345,fd=18))
LISTEN  0       128     127.0.0.1:3306      0.0.0.0:*         users:(("mysqld",pid=2345,fd=17))
```

Listening twice — once on all interfaces, once on localhost. If you need external only — check `bind-address` in the config.

**How many sockets in TIME_WAIT — and who they belong to:**

```bash
ss -s | grep timewait
# or
ss -ant | awk '/TIME-WAIT/ {count++} END {print count}'

# Top destinations leaking TIME-WAIT
ss -tan state time-wait | awk '{print $5}' | sort | uniq -c | sort -rn | head -20
```

> [!TIP]
> High TIME_WAIT is usually normal. If it causes issues — on the client side use `setsockopt` with `SO_LINGER`, or `SO_REUSEADDR` on the server. Flag `-ttu` shows timers.

**Whether a connection is stalled:**

```bash
ss -ti 'dst 10.0.0.50'
```

Check `rtt` and `unacked`. If `unacked` grows but `rtt` stays the same — packets aren't arriving but aren't being lost either. Most likely the remote side stopped reading from the socket.

**Find the process that opened a connection to a specific host:**

```bash
ss -tp 'dst 192.168.1.50'
```

```
State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
ESTAB   0       0       10.0.0.5:45678      192.168.1.50:443   users:(("curl",pid=9876,fd=3))
```

**Aggregated statistics by process:**

```bash
ss -tnp | awk 'NR>1 {print $6}' | sort | uniq -c | sort -rn | head -10
```

Shows how many connections each process holds. Useful for finding processes opening too many sockets.

**Quick reference for everyday tasks:**

```bash
# netstat -tulnp  →  ss -tlnp
# netstat -ulnp   →  ss -ulnp

# What is listening
ss -tlnp

# Active connections
ss -tnp

# Connections to port
ss -t 'dport = :80'

# TIME_WAIT count
ss -s | grep timewait

# Process on port
ss -tlnp 'sport = :8080'

# Keep-alive / TIME_WAIT timers
ss -tno
```
