When rm says a file is busy, a server fails with “address already in use,” or disk space vanishes after you deleted a huge log, the culprit is usually an open file descriptor — not a mystery daemon. That is what lsof is for. It lists open files (and sockets, which look like files to the kernel) and ties each one back to a process.
You do not need every selector on day one. Start by reading the columns, then ask “who has this path?”, “what does this PID hold?”, and “who owns this port?” as the problem narrows. The sections below walk through the commands you will reach for most often — with enough context that each one feels intentional, not magical.
Warm-up: read the columns
Your playground is the live system in front of you. A full lsof with no filters can be huge, so prefer targeted queries. When you do want a broader snapshot, skip DNS and port-name lookups with -nP — triage stays faster and the output is easier to grep.
List open files for your shell (replace the PID with the value of $$):
lsof -nP -p $$
Typical columns look like this (paths and counts will differ):
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 12345 alice cwd DIR 259,2 4096 ... /home/alice
bash 12345 alice rtd DIR 259,2 4096 ... /
bash 12345 alice txt REG 259,2 1396520 ... /usr/bin/bash
bash 12345 alice 0u CHR 136,0 0t0 ... /dev/pts/0
The fields you will use constantly:
- COMMAND / PID / USER — which process owns the handle
- FD — descriptor number, or special values like
cwd,txt,mem - TYPE —
REG(regular file),DIR,CHR,IPv4/IPv6, and so on - NAME — path, device, or socket endpoint
Note: Without sudo, lsof often cannot see other users’ descriptors. If a query for a system service looks empty, retry with sudo lsof … before assuming nothing holds the resource.
Who has this file open
When a deploy cannot overwrite a binary, or umount fails with “target is busy,” ask lsof about that path directly.
Create a demo file and keep it open in the background with tail -f:
echo 'keep me open' > /tmp/lsof-demo.txt
tail -f /tmp/lsof-demo.txt &
TAILPID=$!
Now ask who holds the file:
lsof -nP /tmp/lsof-demo.txt
You should see tail (and your PID) with a numeric FD pointing at the path:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
tail 23456 alice 3r REG 259,2 13 ... /tmp/lsof-demo.txt
Clean up when you are done with this demo:
kill $TAILPID
rm -f /tmp/lsof-demo.txt
To search a whole directory tree for open files, use +D. It walks the tree and is slower on large paths — fine for /var/app, painful for /:
sudo lsof -nP +D /var/log
Note: Prefer a single file path when you already know the busy name. Reach for +D when the error names a directory (mount point, project root) and you need every process still inside it.
By process: what is this PID holding
Sometimes you already know the process — a runaway worker, a stuck service — and you need its open handles. -p lists everything that PID has open; -c matches a command name prefix.
Inspect your current shell again, then compare with a named command:
lsof -nP -p $$
lsof -nP -c ssh
Count how many descriptors a process holds (rough leak signal when the number climbs without bound):
lsof -nP -p $$ | wc -l
When several related PIDs matter, pass them as a comma-separated list:
lsof -nP -p 1234,5678
Note: -c matches the command name (as lsof sees it), not an arbitrary argv string. For a precise PID from ps or systemctl status, prefer -p.
Who is using this port
“Address already in use” means some process still owns the TCP or UDP port. -i selects Internet sockets; :PORT is the everyday form.
Start a throwaway listener so you have something to find:
python3 -m http.server 8765 &
HTTPPID=$!
Ask who is bound to port 8765:
lsof -nP -i :8765
Typical listen line:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python3 34567 alice 5u IPv4 ... 0t0 TCP *:8765 (LISTEN)
Narrow by protocol when the host is noisy:
lsof -nP -iTCP:8765 -sTCP:LISTEN
lsof -nP -iUDP:53
Stop the demo server:
kill $HTTPPID
Note: -nP keeps ports numeric (8765 instead of a service name) so grepping and comparing to error messages stays simple. Tools like ss -lntp answer similar questions; reach for lsof when you already think in “open files” and want one vocabulary for disks and sockets.
Deleted files still held open
Deleting a file removes its directory entry, but the disk blocks stay allocated until every process closes the FD. That is why df can stay high after rm on a huge logfile.
Show open files with a link count less than 1 (often deleted-but-open), or grep for the marker in the NAME column:
sudo lsof -nP +L1
sudo lsof -nP | grep '(deleted)'
A typical hit names the old path and marks it deleted:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 8901 app 4w REG 259,2 5000000000 ... /var/log/app.log (deleted)
Space returns when that process closes the file or exits (restart the service, or truncate via the FD under /proc/<pid>/fd/ in emergencies). Finding the PID with lsof is the first step.
Note: Reach for this pattern when monitoring says the disk is full but du under the mount looks fine — something is still holding a deleted file open.
Everyday combos and permissions
Most real sessions combine a selector with a pipe or a user filter. Limit to one login name with -u:
lsof -nP -u "$USER" | head
Find every TCP listener, then eye the port column:
sudo lsof -nP -iTCP -sTCP:LISTEN
When you care about a path substring across the whole table, filter carefully — lsof output is wide, and a naive grep can false-hit on COMMAND or USER:
sudo lsof -nP | grep '/var/log/nginx'
Note: Incomplete results are usually permissions, not a wrong flag. System daemons need sudo. If you only need “who listens on this port,” lsof -nP -i :PORT is enough; expand to unfiltered dumps only when the targeted query returns nothing useful.
Quick reference card
Keep this nearby until the selectors become muscle memory:
| Goal | Command |
|---|---|
| Faster numeric output | lsof -nP … |
| One file | lsof -nP /path/to/file |
| Directory tree | sudo lsof -nP +D /path |
| One PID | lsof -nP -p PID |
| Command name | lsof -nP -c name |
| Port (any) | lsof -nP -i :8080 |
| TCP listen | lsof -nP -iTCP:8080 -sTCP:LISTEN |
| UDP port | lsof -nP -iUDP:53 |
| One user | lsof -nP -u user |
| Deleted-but-open | sudo lsof -nP +L1 |
| Grep deleted marker | sudo lsof -nP | grep '(deleted)' |
Practice drills
Use your own machine (with sudo when needed). Recreate the file and port demos from earlier if you cleaned them up. The point is to choose the selector with intent, not to memorize syntax under pressure.
- List open files for your current shell with numeric hosts and ports (
$$). - Open
/tmp/lsof-drill.txtwithtail -fin the background, then show which process holds it. - Start something on port
8765and identify the listening COMMAND and PID. - List TCP listening sockets system-wide (privileged).
- Search for deleted-but-open files (or show the
+L1invocation you would use on a full disk).
When you are ready to compare, here are solid answers — not the only ones, but clear and portable:
lsof -nP -p $$
echo drill > /tmp/lsof-drill.txt; tail -f /tmp/lsof-drill.txt &
lsof -nP /tmp/lsof-drill.txt
python3 -m http.server 8765 &
lsof -nP -i :8765
sudo lsof -nP -iTCP -sTCP:LISTEN
sudo lsof -nP +L1
Remember to kill any background tail or http.server you started for the drills.
If you can work through those five comfortably, you already cover most real lsof work: busy files, per-process FD inspection, port conflicts, and deleted-file disk leaks. Start with a path or :port query, add -nP and sudo when the first pass is slow or empty, and widen only when the targeted hit is not enough.