> ## Content Index
> Fetch the complete content index at: https://www.pinglabz.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# netstat vs ss: Why It Was Replaced and How to Switch
- URL: https://www.pinglabz.com/netstat-vs-ss/
- Published: 2026-08-19T05:59:49.000Z
- Updated: 2026-08-19T06:00:17.000Z
- Description: The full command map, wall clock timings on a host holding ten thousand sockets, and the surprise result when you add process lookup.
- Author: Jaime
- Tags: Linux, iproute2, Troubleshooting, Labs

Every Linux networking guide written since about 2011 opens with some version of "netstat is deprecated, use ss." Very few of them say what that actually costs you, what the replacement commands are one for one, or whether it matters on a machine with a normal number of connections. This article does all three, with a benchmark on a host holding ten thousand open sockets, and it finishes with the two tools that neither `netstat` nor `ss` replaces: `lsof` and `fuser`.

All output is real, captured on a Debian 13 host under live traffic through a Cisco Modeling Labs topology. It is part of the [Linux networking commands](https://www.pinglabz.com/linux-networking-commands/) cluster.

## What actually happened to net-tools

`netstat` ships in the `net-tools` package, alongside `ifconfig`, `route` and `arp`. That package was effectively unmaintained through the 2000s, and in 2009 Debian marked it deprecated in favor of `iproute2`. Fedora dropped it from the default install in 2011\. Both packages are still installable and both are on this host:

```
j@llmbits:~$ dpkg -s net-tools | grep -E '^(Package|Version|Description)'
Package: net-tools
Version: 2.10-1.3
Description: NET-3 networking toolkit

j@llmbits:~$ dpkg -s iproute2 | grep -E '^(Package|Version|Description)'
Package: iproute2
Version: 6.15.0-1
Description: networking and traffic control tools
```

Look at the version numbers. `net-tools` 2.10 was released in 2021 after a long gap; `iproute2` tracks the kernel and is at 6.15\. That gap is the whole argument. `net-tools` was written against an interface the kernel has moved past, and it cannot express things the kernel has been able to do for fifteen years.

The technical difference is how they get their data. `netstat` opens text files under `/proc/net` and parses them. `ss` talks to the kernel over a `netlink` socket using the `sock_diag` protocol, which is a binary interface designed for exactly this, supports filtering on the kernel side, and can return per socket TCP state that has no representation in the `/proc` text format at all.

## The command map

Nothing you know is wasted. Every common `netstat` invocation has a direct replacement:

**netstat -tulpn** ss -tulpn Listening sockets with processes. Same flags. 

**netstat -tan** ss -tan All TCP sockets, numeric. 

**netstat -rn** ip route show Routing table. Different tool, not ss. 

**netstat -i** ip -s link Interface counters. 

**netstat -g** ip maddr Multicast group membership. 

**netstat -s** nstat Protocol statistics. nstat shows deltas. 

**netstat -c** watch -n1 ss ... Continuous mode. ss has no -c. 

## Side by side

Listeners, the same moment, both tools:

```
j@llmbits:~$ sudo netstat -tulpn | head -12
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      1272/sshd: /usr/sbi
tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN      8139/cupsd
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      10443/python3
tcp6       0      0 :::22                   :::*                    LISTEN      1272/sshd: /usr/sbi
tcp6       0      0 ::1:631                 :::*                    LISTEN      8139/cupsd
udp        0      0 0.0.0.0:57029           0.0.0.0:*                           896/avahi-daemon: r
udp        0      0 0.0.0.0:5353            0.0.0.0:*                           896/avahi-daemon: r
```

```
j@llmbits:~$ sudo ss -tulpn | head -12
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))
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      5            0.0.0.0:8080       0.0.0.0:*    users:(("python3",pid=10443,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))
```

The same facts, and three differences worth noticing.

`netstat` truncates the process name to a fixed width, so sshd reads as `sshd: /usr/sbi`. `ss` gives the clean process name plus the file descriptor number, which is what you want when correlating with `strace` or `lsof`.

`netstat` shows Send-Q as 0 on every listener. `ss` shows 128, 4096 and 5, which are the real accept backlogs. `netstat` is not reading a different kernel, it simply has nowhere in its output format to put the number.

`netstat` writes `tcp6` and `:::22`; `ss` writes `tcp` with `[::]:22`. The `ss` notation is the standard bracketed form and is much easier to read on a host with real IPv6 addresses.

Established connections through the lab, both tools:

```
j@llmbits:~$ netstat -tan | grep 10.77.3.10 | head -6
tcp        0 230508 10.77.0.100:44766       10.77.3.10:5201         ESTABLISHED
tcp        0 257468 10.77.0.100:44778       10.77.3.10:5201         ESTABLISHED
tcp        0 261512 10.77.0.100:44828       10.77.3.10:5201         ESTABLISHED
tcp        0 217028 10.77.0.100:44802       10.77.3.10:5201         ESTABLISHED
tcp        0      0 10.77.0.100:58176       10.77.3.10:9000         ESTABLISHED
tcp        0 119972 10.77.0.100:44786       10.77.3.10:5201         ESTABLISHED

j@llmbits:~$ ss -tan dst 10.77.3.10 | head -6
State     Recv-Q Send-Q Local Address:Port  Peer Address:Port
ESTAB     0      194112   10.77.0.100:44766   10.77.3.10:5201
ESTAB     0      210288   10.77.0.100:44778   10.77.3.10:5201
ESTAB     0      234552   10.77.0.100:44828   10.77.3.10:5201
ESTAB     0      314084   10.77.0.100:44802   10.77.3.10:5201
ESTAB     0      0        10.77.0.100:58176   10.77.3.10:9000
```

Here the `grep` in the first command is doing something the second does properly. `ss` filtered on `dst 10.77.3.10` in the kernel, so no rows were ever generated for connections that did not match. And a `grep` for an address string will happily match `10.77.3.100` and `110.77.3.10` as well, which `ss dst 10.77.3.0/24` will not.

## Routing and interfaces

`netstat -rn` is where the deprecation actually bites, because the output format cannot represent a modern routing table:

```
j@llmbits:~$ netstat -rn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         192.168.88.1    0.0.0.0         UG        0 0          0 ens192
10.77.0.0       0.0.0.0         255.255.255.0   U         0 0          0 ens224
10.77.0.0       10.77.0.1       255.255.0.0     UG        0 0          0 ens224
192.168.88.0    0.0.0.0         255.255.255.0   U         0 0          0 ens192

j@llmbits:~$ ip route show
default via 192.168.88.1 dev ens192 proto dhcp src 192.168.88.156 metric 101
10.77.0.0/24 dev ens224 proto kernel scope link src 10.77.0.100
10.77.0.0/16 via 10.77.0.1 dev ens224
192.168.88.0/24 dev ens192 proto kernel scope link src 192.168.88.156 metric 101
```

The `netstat` version loses the protocol that installed each route (`proto dhcp` against `proto kernel` matters when you are working out who put a route there), loses the preferred source address, and loses the metric. It also shows two lines both starting `10.77.0.0` with no visual indication that one is a /24 and the other a /16\. It shows nothing at all about policy routing tables, and `netstat` has no equivalent of `ip route get`, which is the single most useful routing command on Linux. That is covered in [managing the Linux routing table](https://www.pinglabz.com/ip-route-linux/).

Interface counters are the one place `netstat` genuinely reads better:

```
j@llmbits:~$ netstat -i
Kernel Interface table
Iface             MTU    RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR Flg
ens192           1500   654499      0 128152 0        109651      0      0      0 BMRU
ens224           1500    48016      0      0 0         49448      0      0      0 BMRU
lo              65536     3151      0      0 0          3151      0      0      0 LRU
```

One line per interface, errors and drops in columns you can scan. `ip -s link` gives the same numbers plus more, but spread over four lines per interface. If you like the `netstat -i` layout, `ip -s -br link` is the closest brief equivalent, and `ifstat` is better than either for watching rates rather than totals. The full net-tools to iproute2 mapping for interfaces is in [ifconfig, route, netstat: legacy net-tools to iproute2](https://www.pinglabz.com/ifconfig-vs-ip/).

## Protocol statistics: netstat -s and nstat

```
j@llmbits:~$ netstat -s | head -18
Ip:
    Forwarding: 2
    313555 total packets received
    14 with invalid addresses
    0 forwarded
    0 incoming packets discarded
    313478 incoming packets delivered
    151917 requests sent out
    132 dropped because of missing route
    OutTransmits: 153597
Icmp:
    2180 ICMP messages received
    20 input ICMP message failed
    ICMP input histogram:
        destination unreachable: 60
        timeout in transit: 1632
        echo requests: 38
        echo replies: 450
```

That `timeout in transit: 1632` is every ICMP time exceeded this host received while the [traceroute](https://www.pinglabz.com/traceroute-linux/) and [mtr](https://www.pinglabz.com/mtr-linux/) work for this cluster was going on, which is a nice illustration of what these counters are: cumulative since boot, and therefore almost useless for answering "is it happening now."

That is what `nstat` fixes. It remembers the last values it printed and shows you the change:

```
j@llmbits:~$ nstat -az | grep -E 'TcpRetransSegs|TcpInSegs|TcpOutSegs|TcpExtTCPLostRetransmit'
TcpInSegs                       152958             0.0
TcpOutSegs                      185504             0.0
TcpRetransSegs                  2006               0.0
TcpExtTCPLostRetransmit         54                 0.0
```

2006 retransmitted segments out of 185504 sent, or about 1.1 percent, on a host that spent the last hour pushing iperf3 through a deliberately impaired link. Run `nstat` without `-a` twice and the second run shows only what changed in between, which turns a boot time counter into a live rate. Nothing in `net-tools` does that.

## The benchmark

Everyone repeats that `ss` is faster. Here is the measurement, on a host holding ten thousand established TCP sockets plus a few thousand in time-wait:

```
j@llmbits:~$ ss -s | head -2
Total: 10488
TCP:   13330 (estab 10003, closed 3322, orphaned 0, timewait 3322)

j@llmbits:~$ ss -tan | wc -l
13331
```

```
j@llmbits:~$ for i in 1 2 3; do time ss -tan > /dev/null; done
real 0.150  user 0.080  sys 0.069
real 0.148  user 0.080  sys 0.069
real 0.149  user 0.074  sys 0.075

j@llmbits:~$ for i in 1 2 3; do time netstat -tan > /dev/null; done
real 0.377  user 0.124  sys 0.253
real 0.375  user 0.128  sys 0.248
real 0.375  user 0.135  sys 0.239
```

Two and a half times faster, and consistent across runs. Look at the `sys` column for the reason: 0.07 seconds of kernel time for `ss` against 0.25 for `netstat`. That is the cost of formatting thirteen thousand socket records as text in `/proc` and parsing them back out again.

Now the part the internet usually omits. Add process lookup:

```
j@llmbits:~$ for i in 1 2 3; do time sudo ss -tanp > /dev/null; done
real 1.079  user 0.341  sys 0.692
real 0.716  user 0.181  sys 0.484
real 0.742  user 0.225  sys 0.490

j@llmbits:~$ for i in 1 2 3; do time sudo netstat -tanp > /dev/null; done
real 0.436  user 0.087  sys 0.317
real 0.462  user 0.099  sys 0.341
real 0.753  user 0.158  sys 0.569
```

`ss -p` is *slower* than `netstat -p` here, roughly 0.7 to 1.1 seconds against 0.4 to 0.8\. Both tools have to walk every process's file descriptors in `/proc` to map socket inodes to processes, because there is no fast kernel interface for that mapping, and neither the netlink path nor anything else helps. The socket enumeration is where `ss` wins, and adding `-p` swamps that win with work both tools do the same slow way.

The practical lesson: **drop `-p` when you are enumerating a lot of sockets and do not need process names.** On a busy load balancer, `ss -tan state time-wait | wc -l` is cheap and `ss -tanp` is not.

And the honest scale caveat: at 13,000 sockets the difference is 0.15 against 0.38 seconds. On a laptop with 40 sockets neither is measurable. If you are keeping `netstat` muscle memory on a workstation, nothing bad is happening to you. The argument for switching is about capability and about the machines where it does matter.

## What netstat simply cannot do

Kernel side filtering

`ss state established '( dport = :443 )'` never generates the rows you do not want. netstat generates everything and you grep.

TCP internals

`ss -i` gives rtt, cwnd, retransmitted bytes and delivery rate per socket. netstat has no equivalent at all.

Socket memory

`ss -m` shows buffer sizes and socket level drops. Nothing in net-tools reports these.

Destroying sockets

`ss -K` kills a matching connection without killing the process. There is no netstat equivalent.

Accept backlogs

ss reports the listen backlog and its current depth. netstat prints zeros.

Modern routing

No route protocols, no metrics, no source hints, no policy tables, and no `route get`.

What `netstat` still has going for it: it is on every ancient box you will ever have to log into, its `-i` table is more readable than `ip -s link`, and `netstat -s` gives a friendlier protocol summary than `nstat -az` if you only want to read it once. Knowing it is not wasted knowledge, it is a portability skill.

## lsof: sockets as files

Neither tool replaces `lsof`, because `lsof` answers a different question. `ss` starts from sockets and finds processes. `lsof` starts from open files, and on Unix a socket is a file:

```
j@llmbits:~$ sudo lsof -nP -iTCP -sTCP:LISTEN
COMMAND   PID USER FD   TYPE DEVICE SIZE/OFF NODE NAME
sshd     1272 root 6u  IPv4   7039      0t0  TCP *:22 (LISTEN)
sshd     1272 root 7u  IPv6   7041      0t0  TCP *:22 (LISTEN)
cupsd    8139 root 6u  IPv6 195177      0t0  TCP [::1]:631 (LISTEN)
cupsd    8139 root 7u  IPv4 195178      0t0  TCP 127.0.0.1:631 (LISTEN)
python3 10443    j 3u  IPv4 390016      0t0  TCP *:8080 (LISTEN)
```

`-n` and `-P` together mean no name resolution and no port name translation, and you want both every time. `-iTCP -sTCP:LISTEN` filters to TCP in the LISTEN state.

The `@host` syntax is where `lsof` is genuinely nicer than `ss`, because it reads like English:

```
j@llmbits:~$ sudo lsof -nP -i @10.77.3.10
COMMAND   PID USER FD   TYPE DEVICE SIZE/OFF NODE NAME
nc      10378    j  3u  IPv4 394047      0t0  TCP 10.77.0.100:58176->10.77.3.10:9000 (ESTABLISHED)
nc      10445    j  3u  IPv4 392065      0t0  TCP 10.77.0.100:58968->10.77.3.10:9000 (ESTABLISHED)
iperf3  10446    j  4u  IPv4 399096      0t0  TCP 10.77.0.100:44762->10.77.3.10:5201 (ESTABLISHED)
iperf3  10446    j  5u  IPv4 399097      0t0  TCP 10.77.0.100:44766->10.77.3.10:5201 (ESTABLISHED)
iperf3  10446    j  7u  IPv4 399098      0t0  TCP 10.77.0.100:44778->10.77.3.10:5201 (ESTABLISHED)
```

And "who has port 8080 open" is a one liner:

```
j@llmbits:~$ sudo lsof -nP -i :8080
COMMAND   PID USER FD   TYPE DEVICE SIZE/OFF NODE NAME
python3 10443    j 3u  IPv4 390016      0t0  TCP *:8080 (LISTEN)
```

The reason to keep `lsof` in the toolbox is that it crosses the boundary. When a process holds a socket *and* a log file *and* a deleted file that is still consuming disk, `lsof -p <pid>` shows all of it in one place. `ss` will never tell you about the deleted file.

## fuser: the fastest possible answer

`fuser` answers exactly one question, which is "which process has this," and it is the quickest tool for it:

```
j@llmbits:~$ fuser -n tcp 8080
8080/tcp:            10443

j@llmbits:~$ sudo fuser -v -n tcp 8080
                     USER        PID ACCESS COMMAND
8080/tcp:            j         10443 F.... python3
```

And it can act on the answer, which is why it exists:

```
j@llmbits:~$ sudo fuser -k -n tcp 8080
8080/tcp:            10443

j@llmbits:~$ sudo ss -tlnp '( sport = :8080 )'
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
```

`fuser -k` sent SIGKILL to the process holding port 8080, and the follow up `ss` confirms nothing is listening there any more. This is the correct fix for the "address already in use" message when a previous run of a service did not exit cleanly, and it beats `ps aux | grep` followed by copying a pid by hand.

Two warnings. `fuser -k` defaults to SIGKILL, not SIGTERM, so the process gets no chance to shut down cleanly. Use `-k -TERM` if that matters. And run it without `-k` first, every single time, because killing the wrong pid on a production box is a bad afternoon. On Debian and derivatives, `fuser` comes from the `psmisc` package and is not always installed by default.

## Which tool for which question

What is listening on this box?

ss -tulpn

Who is holding port 8080 so I can restart the service?

fuser -v -n tcp 8080

Everything this one process has open, sockets and files?

lsof -nP -p <pid>

Is this connection actually moving data, and how well?

ss -tin dst <peer>

Are we retransmitting right now?

nstat (run it twice)

I am on a 2008 box with no iproute2.

netstat -tulpn, and no apology needed

## FAQ

### Is netstat actually being removed?

Not from the archives. `net-tools` is still packaged and installable everywhere. What changed is that it stopped being installed by default on most modern distributions, so a script that shells out to `netstat` will fail on a minimal container image. That is the practical reason to port automation to `ss`, more than any performance argument.

### Are the output formats stable enough to parse?

Neither is a contract. If something downstream needs socket data, use `ss -H` to drop the header and pick fields carefully, or read `/proc/net/tcp` directly, or use the `sock_diag` netlink interface from a real program. Do not build a monitoring system on a `grep` of either tool's human output.

### Why is netstat -p showing blanks for some processes?

Same reason as `ss -p`. Both need root to inspect other users' file descriptors. Neither is broken.

### You said ss is faster, then showed it slower. Which is it?

Enumerating sockets, `ss` is about 2.5 times faster at thirteen thousand sockets, and the gap widens as the count grows. Mapping sockets to processes, both tools do the same expensive `/proc` walk and `ss` showed no advantage in this test. The speed claim is about socket enumeration, and it is worth being precise about that rather than repeating the headline.

### Does any of this apply to netstat on Windows?

No. Windows `netstat` is a completely separate implementation with its own flags, it is not deprecated, and it is the correct tool there. Only the Linux `net-tools` package is what this article is about.

## Key takeaways

- `net-tools` was deprecated because it parses `/proc` text, while `ss` uses the `sock_diag` netlink interface and can filter in the kernel.
- Every common netstat invocation maps directly: `-tulpn` to `ss -tulpn`, `-rn` to `ip route`, `-i` to `ip -s link`, `-s` to `nstat`.
- At 13,000 sockets, `ss -tan` took 0.15 s against `netstat -tan` at 0.38 s. With `-p` the advantage disappears, because both walk `/proc` the same slow way.
- Skip `-p` when you are counting sockets rather than identifying owners.
- `netstat` prints zeros for listen backlogs and has no way to show TCP internals, socket memory, or policy routing. Those gaps are why it was replaced.
- `nstat` shows deltas since its last run, which turns useless boot time counters into a live retransmit rate.
- `lsof -nP -i @host` and `lsof -nP -i :port` are the friendliest syntax for the "who is talking to X" question, and `lsof` crosses from sockets into files.
- `fuser -v -n tcp <port>` then `fuser -k -n tcp <port>` is the fastest fix for a port held by a stale process. Run it without `-k` first, and remember it defaults to SIGKILL.

For everything `ss` can do once you commit to it, including the filter language and the per socket TCP internals, read [ss, the modern socket statistics tool](https://www.pinglabz.com/ss-command-linux/). The interface and routing halves of the same migration are covered in [ifconfig, route, netstat to iproute2](https://www.pinglabz.com/ifconfig-vs-ip/) and [ip route](https://www.pinglabz.com/ip-route-linux/). Everything in this series is indexed on the [Linux networking commands guide](https://www.pinglabz.com/linux-networking-commands/).