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 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 toolsLook 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:
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: rj@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:9000Here 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 101The 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.
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 LRUOne 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.
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: 450That timeout in transit: 1632 is every ICMP time exceeded this host received while the traceroute and mtr 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.02006 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
13331j@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.239Two 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.569ss -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
ss state established '( dport = :443 )' never generates the rows you do not want. netstat generates everything and you grep.ss -i gives rtt, cwnd, retransmitted bytes and delivery rate per socket. netstat has no equivalent at all.ss -m shows buffer sizes and socket level drops. Nothing in net-tools reports these.ss -K kills a matching connection without killing the process. There is no netstat equivalent.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.... python3And 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:PortProcessfuser -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
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-toolswas deprecated because it parses/proctext, whilessuses thesock_diagnetlink interface and can filter in the kernel.- Every common netstat invocation maps directly:
-tulpntoss -tulpn,-rntoip route,-itoip -s link,-stonstat. - At 13,000 sockets,
ss -tantook 0.15 s againstnetstat -tanat 0.38 s. With-pthe advantage disappears, because both walk/procthe same slow way. - Skip
-pwhen you are counting sockets rather than identifying owners. netstatprints zeros for listen backlogs and has no way to show TCP internals, socket memory, or policy routing. Those gaps are why it was replaced.nstatshows deltas since its last run, which turns useless boot time counters into a live retransmit rate.lsof -nP -i @hostandlsof -nP -i :portare the friendliest syntax for the "who is talking to X" question, andlsofcrosses from sockets into files.fuser -v -n tcp <port>thenfuser -k -n tcp <port>is the fastest fix for a port held by a stale process. Run it without-kfirst, 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. The interface and routing halves of the same migration are covered in ifconfig, route, netstat to iproute2 and ip route. Everything in this series is indexed on the Linux networking commands guide.