Two questions send most people to netstat on a Mac: which ports are listening, and which process holds one open. On macOS 26.5.2, the first has a one-line answer; the second doesn’t involve netstat at all:
- List every listening port:
netstat -an | grep LISTEN - Find the process/PID behind a specific port:
lsof -i :PORT(orlsof -iTCP -sTCP:LISTEN -n -Pfor all of them at once) — BSDnetstathas no flag that prints a PID column
The second point costs people the most time. On Linux, netstat -tulpn gets both answers in one command. On macOS, -p takes a protocol name, not a process, and no netstat combination reaches a PID. If you came here from netstat macos show pid, lsof is the actual tool — not a substitute for netstat, the tool for this job.
$ netstat -an | grep LISTEN | head -5
tcp4 0 0 127.0.0.1.24282 *.* LISTEN
tcp46 0 0 *.11434 *.* LISTEN
tcp4 0 0 127.0.0.1.63885 *.* LISTEN
tcp4 0 0 127.0.0.1.14023 *.* LISTEN
tcp4 0 0 127.0.0.1.14022 *.* LISTEN
$ netstat -an | grep -c LISTEN
74
74 listening sockets on this machine right now — 54 tcp4, 11 dual-stack tcp46, 9 tcp6. That breakdown will differ on your Mac, and differ again an hour from now; it’s a live table, not a fixed spec. What’s stable is the command.
For the PID:
$ lsof -i :7000 -P -n
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
ControlCe 1397 osxhub 9u IPv4 0xXXXXXXXXXXXXXXXX 0t0 TCP *:7000 (LISTEN)
ControlCe 1397 osxhub 10u IPv6 0xXXXXXXXXXXXXXXXX 0t0 TCP *:7000 (LISTEN)
The USER and DEVICE columns are redacted here; every other value is the real output. Port 7000, PID 1397, process name truncated to nine characters by lsof‘s column width — that’s Control Center’s AirPlay receiver on this Mac, and it’ll be something else on yours. Swap the port number and any listener’s PID falls out the same way.
Why netstat can’t do the second half
macOS ships the BSD netstat, not the Linux net-tools one — same name, different program. Linux’s -p means “show program name and PID.” BSD’s -p means “show statistics for this protocol,” and it wants an argument:
$ netstat -p
-p: option requires an argument -- p
Usage: netstat [-AaLlnW] [-f address_family | -p protocol]
netstat [-gilns] [-f address_family]
netstat -i | -I interface [-w wait] [-abdgRtS]
netstat -s [-s] [-f address_family | -p protocol] [-w wait]
$ echo $?
64
netstat -p tcp parses fine and shows only TCP connections — no PID column, because the socket table BSD’s kernel exposes here was never wired to a process ID. That’s a design boundary, not a missing flag. lsof and nettop read a different data source that does carry the process link, which is why they can answer a question netstat structurally can’t.
-l doesn’t mean “listening”
This is the flag most likely to burn a former Linux user — netstat -l is close to reflex. The man page entry, quoted in full, is four words:
-l Print full IPv6 address.
A column-width switch for IPv6 formatting, with no filter logic for socket state:
$ netstat -l | grep -c LISTEN
0
Zero — not because nothing is listening (74 sockets are, per the earlier count), but because -l was never a listening-socket filter, and the command exits 0 either way. No error to notice. The uppercase -L is closer to the Linux idea — it prints listen queue sizes — but alone it falls back to routing sockets; paired with -f inet it does something closer to what “netstat -l” implies:
$ netstat -aL -f inet | head -3
Current listen queue sizes (qlen/incqlen/maxqlen)
Listen Local Address
0/0/128 localhost.24282
That’s queue depth, not a port list, and it won’t line up row-for-row with netstat -an | grep LISTEN — they count different things. For the port list itself, the plain grep from the top of this article is still the direct route.
-u and -t aren’t documented, and both still run
Checked with a word-boundary search rather than a plain substring match (substring matching would false-positive on -u inside longer option names), man netstat on this machine contains -u and -t zero times each — not as entries, not in a synopsis line, not in passing. The full documented option list is 24 flags:
-A -a -B -b -c -d -f -g -I -i -L -l -m -n -p -q -r -R -s -S -v -W -w -x
No -u. No -t. Both nonetheless parse and return exit 0:
$ netstat -u | head -1
Active LOCAL (UNIX) domain sockets
$ netstat -u | grep -c udp
0
-u shows UNIX-domain sockets, not UDP. man netstat documents -f address_family with four recognized values: inet, inet6, unix, and vsock. Running netstat -u and the documented netstat -f unix back to back returns identical line counts — 845, for both, at the same moment — so -u behaves as an undocumented shorthand for -f unix. The actual UDP sockets are one flag away:
$ netstat -an -p udp | grep -c udp
116
Zero from the flag named -u, 116 from the one that actually means UDP. That gap is what makes this dangerous in a script: a netstat -u-based check reports a quiet network forever, with no error to flag the mistake.
-t is a smaller trap because it’s closer to a no-op. netstat -t and plain netstat with no arguments produce identical output, aside from a couple of lines of live UNIX-socket churn between the two runs — same “Active Internet connections” header, same lack of listening sockets, since neither includes -a. The lowercase t appears in the man page exactly once, inside the interface-display synopsis cluster [-abdgqRtS]. There, it does something: paired with -i, it adds a Time column to the per-interface stats table. Outside that context, it’s along for the ride.
The two flags that fail loudly, and why that’s the safer half
-p and -c share a shape: both require an argument, so both refuse to run bare, both print option requires an argument, and both exit 64. -c reached for as a “continuous output” habit turns out to be a queue filter that needs -q and an interface to mean anything — continuous polling on macOS is -w wait instead:
$ netstat -c
-c: option requires an argument -- c
$ echo $?
64
An error and a 64 are things a script can catch and a person can paste into a search box — strictly better than what -u, -l, and -t do, which is exit 0 while quietly answering a different question. The lesson isn’t “avoid -p and -c“; it’s that a clean exit code confirms a flag parsed, not that it matched your Linux-shaped expectation of it.
Linux habits mapped to macOS
| Linux habit | On macOS 26.5.2 | What works instead |
|---|---|---|
netstat -tulpn | Invalid — -p demands a protocol argument, errors immediately | netstat -an | grep LISTEN plus lsof -iTCP -sTCP:LISTEN -n -P |
netstat -lntp | -l is an IPv6 formatting switch; still no PIDs | same as above |
ss -tulpn | ss isn’t installed — it ships with Linux’s iproute2 | lsof -i for PIDs, netstat -an for the raw socket table |
| find PID for a port | no netstat flag reaches a PID here | lsof -i :PORT |
command -v ss on this machine returns nothing and a nonzero exit — it’s simply absent, not aliased or renamed. There’s no single-binary macOS equivalent; the two jobs ss does are split across netstat for socket listings and lsof for the process link.
Why the man page won’t warn you
None of this is a documentation bug. man netstat describes BSD’s netstat accurately — -l genuinely prints full IPv6 addresses, in exactly those words. What it can’t do is describe what a flag means on a different OS’s version of the same command name. Reading it top to bottom catches -l, since it’s documented, but not -u or -t, because there’s nothing written down to catch. All three share one shape: a correctly documented (or undocumented) flag, silently answering a different question, exiting 0 either way.
Every command here is read-only. Run them on your own Mac and the port list, the socket counts, and the PIDs will all differ from what’s shown above — what won’t differ is which flags quietly change subject and which ones stop you at the door.