lsof lists open files, and on macOS “open file” includes network sockets — so it answers two questions people reach for it constantly: what’s using a port you need, and what’s quietly holding a file open. The port case is the one most people arrive for, so start there. Every command below is read-only except the one kill, and all were run on macOS 26.5.2.
What’s using a port — and how to free it
A dev server won’t start because the port’s taken — “address already in use” on 3000, 8080, whatever it is. Point lsof at the port with -i:
$ lsof -i :3000 -P -n
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Python 60458 osxhub 4u IPv6 0xXXXXXXXXXXXXXXXX 0t0 TCP *:3000 (LISTEN)
The USER and DEVICE columns are redacted; everything else is the real output. That’s the process holding port 3000 — its PID is 60458, and (LISTEN) confirms it’s the one accepting connections. The two flags matter: -P keeps ports numeric, because without it lsof prints the IANA service name instead — port 8000 shows as irdmi, 3306 as mysql — which is the opposite of helpful when you’re hunting a number. -n skips reverse-DNS on addresses, so the command returns immediately instead of stalling on lookups.
To free the port, you need only the PID, and -t (terse) prints exactly that — nothing else, one PID per line, made for piping:
$ lsof -ti :3000
60458
$ lsof -ti :3000 | xargs kill
lsof -ti :3000 emits the PID; xargs hands it to kill. If the port has more than one listener, -t prints every PID and xargs passes them all to a single kill, so this frees the port in one line regardless of how many processes hold it. A plain kill sends SIGTERM, which lets the process shut down cleanly; if it ignores that and the port stays occupied, lsof -ti :3000 | xargs kill -9 forces it — but -9 gives the process no chance to release resources or save state, so reach for it only after the polite version fails. Check your aim first by running the lsof -ti half alone: if it prints nothing, no process owns that port and there’s nothing to kill. For a fuller treatment of listening ports — every socket at once, and why netstat can’t print a PID on macOS — see how to list listening ports and find the process on macOS.
Space that df counts and du can’t find
lsof‘s other half is files, and it answers one that no other everyday tool does: what’s holding disk space you can’t account for. A process can keep a file open after the file is deleted. The directory entry disappears, but the data stays on disk until the last open descriptor closes — so df still counts the space while du, Finder, and find show nothing, because there’s no longer a name for them to list. On Linux, lsof tags these with (deleted) in the NAME column. macOS lsof (version 4.91) doesn’t:
$ lsof -p 37050 -a -d 3
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Python 37050 osxhub 3r REG 1,15 41943040 42131109 /private/tmp/lsofdemo/big.bin
That file was deleted before the command ran. lsof still prints its original path with no marker, and SIZE/OFF shows the 40 MB it’s still occupying. The one reliable signal that it’s unlinked is the link count, which +L1 adds as an NLINK column and filters to files where it’s below 1:
$ lsof -p 37050 -a +L1
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
Python 37050 osxhub 3r REG 1,15 41943040 0 42131109 /private/tmp/lsofdemo/big.bin
NLINK 0 is the tell. To chase a disappearing-space problem across the whole system rather than one known PID, sudo lsof +L1 lists every unlinked-but-open file — but expect noise: on this machine, in an ordinary desktop session, that returned 894 rows, most of them small system caches that are unlinked by design. Sort on SIZE/OFF and look for the outlier. The space comes back when that process exits or is killed — no reboot, no rm, because the name is already gone.
Why narrowing an lsof query can widen it
lsof combines selection criteria with OR, not AND — the opposite of most command-line tools, and the reason the deleted-file query above needed -a. Adding -p PID to a search doesn’t filter it down; it adds that PID’s files to whatever you already asked for. The same two criteria, one flag apart:
lsof +L1 → 894 files (every unlinked file on the system)
lsof +L1 -p 37050 → 901 files (those 894 PLUS PID 37050's own descriptors)
lsof +L1 -a -p 37050 → 1 file (only the unlinked file PID 37050 holds)
Without -a, the count goes up when you add a PID, because 894 unlinked files plus that process’s seven open descriptors is 901. With -a, it’s the intersection — the single 40 MB file this process is sitting on. Any time an lsof query returns far more than you expected, a missing -a is the first thing to check.
+d and +D scan to different depths
Two flags scan a directory for open files, and the difference is easy to get backwards. +d descends one level; +D recurses the entire tree:
$ lsof +d /private/tmp/lsofdemo/logs
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
tail 32056 osxhub 3r REG 1,15 2 42130873 /private/tmp/lsofdemo/logs/top.txt
$ lsof +D /private/tmp/lsofdemo/logs
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
tail 32056 osxhub 3r REG 1,15 2 42130873 /private/tmp/lsofdemo/logs/top.txt
tail 32057 osxhub 3r REG 1,15 2 42130874 /private/tmp/lsofdemo/logs/sub/deep.txt
+d missed sub/deep.txt; +D caught it. For “what’s keeping this whole folder tree busy,” +D is the one you want, at the cost of walking every subdirectory — slow on a large tree. +d is faster when you already know the file sits directly in the named folder. One more note on scope: lsof doesn’t need sudo to list your own open files — lsof -v reports “Anyone can list all files” on this build — but it only sees other users’ and system processes’ handles when run with sudo. A port your own dev server opened is usually yours to see plainly; a system-wide +L1 sweep needs sudo to be complete.
The invocations worth keeping: what’s on a port is lsof -i :PORT -P -n, and lsof -ti :PORT | xargs kill frees it; space that df counts and du can’t find is lsof +L1 read through the NLINK column; and a query that balloons instead of narrowing is a missing -a. man lsof documents every flag here accurately — it just organizes them by flag, not by the problem that sent you looking.