netstat Command: Tutorial & Examples
The museum piece you still need to know, because every tutorial written before 2015 starts with it.
The netstat command is the classic Linux way to ask "what is my network actually doing right now?" It shows you open TCP and UDP connections, listening ports, the kernel routing table, interface counters, and a dozen other views — all from one binary. It shipped with BSD net-tools in 1991, was ported to Linux a few years later, and for two decades it was how you looked at network state on a server. Then iproute2 arrived and quietly replaced it. Today netstat is deprecated, but it lives on in every old StackOverflow answer, every dog-eared sysadmin book, and the muscle memory of every admin over 35. Learn it anyway — you will read its output for the rest of your career.
Note
netstatlives in thenet-toolspackage, which is not installed by default on modern Debian, Ubuntu, Fedora, or Arch. If you typenetstatand get "command not found," that's the message: the distro is telling you to learnssandipfrom theiproute2suite instead. Installnet-toolsif you must (apt install net-tools), but treat it as a museum visit, not a daily tool.
Your First Look
The single most famous invocation in Linux networking:
netstat -tulpn
That flag soup decodes as tcp + udp + listening + program (the process that owns the socket) + numeric (don't resolve names). It answers the question every admin asks ten times a day: "what is listening on this server, and which program owns it?"
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN 812/mysqld
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 654/sshd
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1124/nginx
tcp6 0 0 :::443 :::* LISTEN 1124/nginx
udp 0 0 0.0.0.0:68 0.0.0.0:* 432/dhclient
Five lines, the whole story: MySQL safely on 127.0.0.1 (loopback only), SSH and nginx on 0.0.0.0 (every interface, public-facing), DHCP client waiting for a lease. If MySQL had said 0.0.0.0:3306 you'd close the laptop and reach for a firewall — that's the netstat glance that has saved a thousand databases.
How I Read It
I run netstat -tulpn first, always, on any server I've inherited. My eyes go straight to the Local Address column. Anything bound to 127.0.0.1 is safe — only this machine can reach it. Anything bound to 0.0.0.0 (or :: for IPv6) is exposed to every network the box is on, and I need a firewall reason for each one to exist. A specific IP like 10.0.0.5 means "only the private network" — fine for an internal service.
Then I scan the PID/Program name column. Every listening port should map to a program I expected. An unfamiliar name on an open port is how you discover the previous admin left a Redis on 0.0.0.0:6379 with no password.
For established connections (drop the -l) I run netstat -tnp and look at State. A pile of TIME_WAIT is normal on a busy server. A pile of CLOSE_WAIT means your application forgot to close sockets — a real bug. A pile of SYN_RECV to one port from many IPs is a SYN flood.
For routing I still reach for netstat -rn before ip route — the columnar format is just easier to read at 2 a.m. Same for netstat -i versus ip -s link.
The Output, Explained
The default view (netstat with no flags) shows active connections only — sockets in ESTABLISHED state plus Unix domain sockets. To see listeners too, add -a. To see only listeners, use -l. To filter by protocol, add -t (TCP) or -u (UDP). To map sockets back to processes, add -p (requires root for sockets you don't own). To skip slow DNS reverse lookups, always add -n.
The columns:
- Proto —
tcp,tcp6,udp,udp6,unix,raw. The6suffix means IPv6. - Recv-Q — bytes the kernel has buffered for your application to read. Persistently non-zero means your app isn't reading fast enough.
- Send-Q — bytes you've written that the kernel hasn't yet shipped (or the peer hasn't acknowledged). Persistently non-zero often means the network or the peer is slow.
- Local Address —
IP:porton this machine.0.0.0.0= all IPv4 interfaces,::= all IPv6,127.0.0.1= loopback only. - Foreign Address —
IP:porton the other end, or0.0.0.0:*for a listener (nobody connected yet). - State — for TCP only. The interesting ones:
LISTEN(waiting for connections),ESTABLISHED(open and active),TIME_WAIT(we closed it, waiting in case stray packets arrive — normal),CLOSE_WAIT(peer closed, we haven't — usually a bug in your code),SYN_SENT/SYN_RECV(handshake in progress). - PID/Program name — only with
-p; the process holding the socket.
Reading It by Example
A gallery of readouts and what they mean — pattern recognition is most of the job.
tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN 812/mysqld
MySQL bound to every interface. If this server has a public IP and no firewall, your database is on the internet. Rebind to 127.0.0.1 in my.cnf (bind-address = 127.0.0.1) and restart. This is one of the most common first-server mistakes, and netstat -tulpn is how you catch it.
tcp 0 98304 10.0.0.5:443 203.0.113.7:51234 ESTABLISHED 1124/nginx
A persistently large Send-Q (98 KB sitting unsent) means nginx wrote data the kernel hasn't been able to ship — usually the client is slow, on a bad network, or has stopped reading. One such row is fine; hundreds of them is a stuck-clients problem worth investigating.
tcp 0 0 10.0.0.5:8080 10.0.0.9:44312 CLOSE_WAIT 2341/myapp
tcp 0 0 10.0.0.5:8080 10.0.0.9:44318 CLOSE_WAIT 2341/myapp
tcp 0 0 10.0.0.5:8080 10.0.0.9:44322 CLOSE_WAIT 2341/myapp
A pile of CLOSE_WAIT against one process is almost always a socket leak in that application — the peer closed, but the app forgot to call close(). Restart buys you time; fixing the code is the real answer. Pair with lsof -p 2341 to confirm the FDs are piling up.
tcp 0 0 0.0.0.0:80 198.51.100.4:39482 SYN_RECV
tcp 0 0 0.0.0.0:80 198.51.100.4:39484 SYN_RECV
tcp 0 0 0.0.0.0:80 198.51.100.4:39486 SYN_RECV
Many SYN_RECV from a small set of IPs — that's a SYN flood. Enable tcp_syncookies (sysctl -w net.ipv4.tcp_syncookies=1) and put the offender in your firewall.
Cheat Sheet
netstat -tulpn # the famous one: all listeners, TCP+UDP, with PIDs, numeric
netstat -tnp # established TCP connections with owning processes
netstat -an | grep :80 # who is talking to port 80 right now
netstat -rn # kernel routing table (still more readable than `ip route`)
netstat -i # interface counters: RX/TX packets, errors, drops
netstat -s # protocol statistics: retransmits, resets, listen overflows
netstat -c # keep refreshing every second (poor man's watch)
netstat -W # wide output — don't truncate long IPv6 addresses
Pro Tip
even devotees of
iproute2often keepnet-toolsinstalled for two views:netstat -rn(routing table) andnetstat -i(interfaces). The column alignment is genuinely easier on the eyes thanip routeorip -s link, and for read-only inspection there's no downside. Usessfor sockets,ipfor configuration,netstatfor those two readable summaries.
How You'll Actually Use It
Three real moments:
"What's listening?" — netstat -tulpn. The first thing I run on any server. Five seconds, total clarity on the attack surface.
"Why is this connection stuck?" — netstat -tnp | grep <peer-ip>. Shows the state of every connection to that peer plus the owning process. Combined with lsof -i (which shows the same data through the file-handle lens) and tcpdump on the wire, you can diagnose almost any "it hangs" report.
"Did the route get installed?" — netstat -rn after bringing up a WireGuard tunnel or a new bridge. Default gateway, per-subnet routes, the metric column — all there.
Gotchas
netstat may simply not exist. On a fresh Debian 12 or Ubuntu 24.04, the binary isn't installed; the distro expects you to use ss. If you're writing a script that has to run anywhere, use ss — it's part of the base system.
netstat -p requires root for sockets you don't own. Without sudo the PID column is blank for everyone else's connections.
Without -n, netstat does a reverse DNS lookup for every address. On a server with a slow or broken resolver, this can take minutes. Always pass -n unless you specifically want names.
netstat is slow on busy servers — and this is the architectural punchline below. On a box with 50,000 connections, netstat -tnp can take 10+ seconds while ss -tnp returns instantly. Same data, different transport.
Output formats differ subtly between BSD netstat, Linux netstat (net-tools), and the BusyBox version on Alpine and embedded systems. Scripts that parse netstat output are fragile across distributions.
History & Philosophy
Here is the magic, and it's the kind of detail that makes you love Unix a little more. netstat is the last common tool that still reads its data from text files in /proc the old way. When you run netstat -tn, under the hood it opens /proc/net/tcp, reads a text table, and pretty-prints it. Try it yourself:
cat /proc/net/tcp | head
That's the raw connection table, in plain ASCII, columns of hex-encoded addresses and ports. The kernel writes it; netstat parses it. This is the "everything is a file" philosophy of Unix at its purest — network state isn't behind some API, it's a file you can cat. To map a socket back to a process, netstat -p then walks /proc/<PID>/fd/ for every process on the system, calling readlink on each file descriptor to find the matching socket inode. On a server with 500 processes and 50,000 connections, that's a lot of syscalls.
ss does the same job differently. Instead of parsing /proc, it opens a netlink socket to the kernel and asks "give me the whole TCP table" — one syscall, kernel hands back the full structured table in binary. No /proc walks, no text parsing, no /proc/<PID>/fd scan. Same data, different transport. That's why ss is dramatically faster on busy machines, and that's why iproute2 gradually replaced net-tools across the 2010s. The old tools weren't wrong — netlink just didn't exist when they were written. netstat is a perfect fossil of how Linux exposed kernel state before netlink: through the filesystem, in human-readable text. Keep it installed. Read its output. But for new scripts, write ss.
Reference
Column legend for the netstat -tulpn layout:
| Column | Meaning |
|---|---|
Proto |
tcp, tcp6, udp, udp6, unix, raw — the 6 suffix means IPv6 |
Recv-Q |
Bytes received but not yet read by the app — growing = the app isn't reading fast enough |
Send-Q |
Bytes sent but not yet ACKed by the peer — growing = the network or peer is stuck |
Local Address |
IP:port on this machine — 127.0.0.1 = loopback only, 0.0.0.0/:: = all interfaces |
Foreign Address |
IP:port on the other end, or 0.0.0.0:* for a listener with no peer yet |
State |
TCP connection state (see below); blank for UDP |
PID/Program name |
The process holding the socket — only shown with -p |
TCP states in the State column:
| State | Meaning | What it signals |
|---|---|---|
LISTEN |
Waiting for incoming connections | A service is open on this port |
ESTABLISHED |
Open and active | Normal traffic in flight |
SYN_SENT |
Sent a SYN, awaiting reply | This box is connecting out |
SYN_RECV |
Got a SYN, sent SYN-ACK | Handshake mid-way; many from few IPs = SYN flood |
TIME_WAIT |
Recently closed, holding the grace window | Normal in bulk on a busy server |
CLOSE_WAIT |
Peer closed, local app hasn't called close() |
Often an app bug when it piles up — a socket leak |
See Also
- ss — the modern replacement; learn this for new work
- ip —
iproute2's answer toroute,ifconfig,arp, and friends - ifconfig — deprecated sibling of
netstat, replaced byip addr/ip link - route — deprecated sibling, replaced by
ip route - lsof — same socket data through the "everything is a file" lens
- ps — once
netstat -pgives you a PID,pstells you the rest - tcpdump — when you need to see the actual packets, not just the connections
- /proc — the filesystem
netstatreads from;cat /proc/net/tcpfor the raw view - TCP, UDP, port, socket — the concepts behind the columns
CleverUptime runs a one-liner that uses ss (with netstat as fallback on older boxes) to inventory every listener on your server, then automatically configures external port scans so you find out the moment something binds to 0.0.0.0 that shouldn't.
Inherited a server and don't know what's listening on it?
CleverUptime runs the modern equivalent of
netstat -tulpnfor you, in plain language, and flags every public-facing port with the program that owns it — so an accidental0.0.0.0bind never reaches production.Want to see your own server's health right now? One command, no signup, no install.