lsof Cheat Sheet for Mac: Ports, PIDs, Listeners and Gotchas
To list every listening TCP port on a Mac with the process behind it, run:
lsof -nP -iTCP -sTCP:LISTEN
For one port, use lsof -nP -iTCP:3000 -sTCP:LISTEN. For just the PID, use lsof -ti tcp:3000 -sTCP:LISTEN. Everything here is for the lsof that ships with macOS (4.91 on macOS 27.2), which differs from the Linux one.
lsof commands for ports on a Mac
| Task | Command |
|---|---|
| All listening TCP ports | lsof -nP -iTCP -sTCP:LISTEN |
| One port, listener only | lsof -nP -iTCP:3000 -sTCP:LISTEN |
| Several ports | lsof -nP -iTCP:3000,5173 -sTCP:LISTEN |
| A range of ports | lsof -nP -iTCP:3000-3010 -sTCP:LISTEN |
| Just the PID | lsof -ti tcp:3000 -sTCP:LISTEN |
| Every socket on a port, clients included | lsof -nP -i :3000 |
| UDP sockets | lsof -nP -iUDP |
| Listeners of one program | lsof -nP -a -c node -iTCP -sTCP:LISTEN |
| Sockets of one PID | lsof -nP -a -p 3597 -i |
| Working directory of a PID | lsof -a -p 3597 -d cwd |
| Full command line of a PID | ps -o command= -p 3597 |
| Full process names | lsof +c 0 -nP -iTCP -sTCP:LISTEN |
| Wait until a port is free | lsof -nP -iTCP:3000 -sTCP:LISTEN +r 1 >/dev/null |
| Every user's listeners | sudo lsof -nP -iTCP -sTCP:LISTEN |
Pass -n and -P every time. -n skips reverse DNS lookups, which on a cold run is the difference between about a second and 0.05 seconds. -P keeps port numbers. Without it, port 3000 prints as *:hbci and 8080 as *:http-alt, and grep ':3000' finds nothing.
If you come from Linux, the first row is the Mac's ss -tlnp. netstat -tulpn doesn't translate, because on macOS netstat's -p takes a protocol, not a process.
How to read lsof output
lsof -nP -iTCP -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
rapportd 716 alex 11u IPv4 0xa4b9aa9c966eb26d 0t0 TCP *:49178 (LISTEN)
Raycast 2286 alex 40u IPv4 0x28c2ed5af4c205c3 0t0 TCP 127.0.0.1:7265 (LISTEN)
node 3597 alex 17u IPv6 0x8cc6e94f298c8068 0t0 TCP *:3000 (LISTEN)
node 3944 alex 14u IPv4 0x98eab4974281a3a0 0t0 TCP 127.0.0.1:9229 (LISTEN)
The columns to read are COMMAND, PID, USER and NAME. COMMAND is cut to 9 characters, and +c 0 prints it in full. Names you don't recognize, like rapportd, are mostly Apple's: see Apple processes listening on a Mac.
In NAME, *:3000 means every interface, so other machines on your network can reach the port unless a firewall stops them. 127.0.0.1:9229 or [::1]:5173 is loopback, reachable only from this Mac.
An IPv6 line with *:3000 is usually a dual-stack socket that accepts IPv4 too. Node's server.listen(3000) opens one, so 127.0.0.1:3000 still connects.
Find which process is using a port
Add -sTCP:LISTEN. Without it, lsof also returns every connection to the port, the client side included. Here nc is connected to a Node server:
lsof -nP -i :3000
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 3597 alex 17u IPv6 0x8cc6e94f298c8068 0t0 TCP *:3000 (LISTEN)
node 3597 alex 21u IPv6 0x68920d4f7fad4691 0t0 TCP 127.0.0.1:3000->127.0.0.1:53024 (ESTABLISHED)
nc 4012 alex 3u IPv4 0xb55d6ed30db143e 0t0 TCP 127.0.0.1:53024->127.0.0.1:3000 (ESTABLISHED)
Here lsof -ti :3000 prints two PIDs. Usually the second one is your browser, and kill -9 $(lsof -ti :3000) can take the browser's helper process down with the server. With the filter you get only the server:
lsof -ti tcp:3000 -sTCP:LISTEN
3597
Write tcp:3000, not :3000. UDP sockets have no state, so -sTCP:LISTEN doesn't filter them out.
-t prints bare PIDs, and lsof exits with 1 when nothing matches, which makes a quick check:
lsof -ti tcp:3000 -sTCP:LISTEN >/dev/null && echo "port 3000 is taken"
To end the process safely, see how to kill the process on a port.
Filter by process name or PID
lsof -nP -a -c node -iTCP -sTCP:LISTEN
lsof ORs its selection options. Without -a, this asks for every file any node process has open, plus every listening TCP socket on the system, and prints hundreds of lines. With -a you get node's listeners only. The same goes for -p.
-c node matches any name that starts with node. For an exact match, use -c '/^node$/'.
Find the folder and command line behind a PID
Three node rows look the same until you know where each one runs. lsof has the working directory. For the arguments you need ps, because lsof never shows them:
lsof -a -p 3597 -d cwd -Fn | sed -n 's/^n//p'
ps -o command= -p 3597
/Users/alex/code/shop-api
node /Users/alex/code/shop-api/server.js --port 3000
-Fn switches lsof to one field per line, and sed keeps the path.
Why lsof shows nothing for a port that is in use
The process probably belongs to root or another user. Without sudo, lsof lists only your own processes, and on macOS it always has. A caddy that runs as root on port 443 doesn't show up. With sudo it lists everyone's:
sudo lsof -nP -iTCP -sTCP:LISTEN
netstat used to be the way to look without sudo. On macOS 27 it can print no ports at all, and nettop -L 1 -m tcp -n is what still shows every user's sockets.
Portis puts these lookups in one table. Type 3000 or a folder name like shop-api in the search field, and the matching row shows the PID, user, address, working directory and full command line. Root's listeners are in the list without sudo.