lsof

List open files and the processes holding them

Updated 2026-09-11

lsof lists open files. That word covers more than it sounds like: a regular file, a directory, a device, a pipe, a network socket and the program's own executable are all open files, and lsof reports on every kind in one table. It answers "what is holding this" about a file you cannot unmount over, a port you cannot bind, and space that will not come back.

The whole table is almost never what you want. With no arguments it prints every open file belonging to every process, which on an idle Debian server is already thousands of lines, most of them shared libraries. A useful invocation narrows: to one path, one process name, one user, one port.

Narrowing has a trap in it. -c, -u, -p, -i and -d combine with OR, so lsof -c nginx -u www-data lists everything nginx has open plus everything www-data has open. -a switches the combination to AND, and nothing makes it the default. A command that reads as two conditions is answering a looser question until the -a is in it.

The FD column carries most of the detail. A number is a file descriptor, followed by the mode the file was opened in: 3w is descriptor 3 open for writing, 3r for reading, 3u for both. Four names appear in place of a number. cwd is the process's working directory, rtd its root, txt the executable it is running, and mem a file mapped into its address space, which for anything dynamically linked means a dozen shared libraries. -d ^mem drops those, and it is worth typing before reading anything.

A file that has been unlinked while a process still holds it open is the case nothing else will show you. The name is gone from its directory, so ls cannot see it and du does not count it, while the blocks stay allocated until the last descriptor closes. df goes on reporting them as used, which is why a filesystem can stay full after the large file has been deleted, and why restarting the process frees the space when deleting the file did not. lsof +L1 lists the files in that state and nothing else.

-i selects network files. lsof -i :8080 names the process listening on that port and anything connected to it, which is the same ground ss covers from the socket's side rather than the process's. Addresses and ports are resolved to names by default, so 8080 prints as http-alt and 127.0.0.1 as localhost; -P and -n turn off the port and address lookups, and a -i with both is easier to read as well as faster.

What lsof can see depends on who runs it. Open files are read through /proc, where an ordinary account may read only its own processes, so the same command without sudo gives a shorter answer rather than an error. That is worth knowing before concluding that nothing holds a file.

Debian ships lsof in a package of its own, so a minimal install may not have it. fuser comes from psmisc and answers a narrower version of the same question in a form that is easier to pipe into a kill.

Sample files used on this page

Every example below was run against these files. Recreate them to follow along.

the files under /srv the setup script starts six small Python services and every open file on this page belongs to one of them. tips-log appends to app.log, tips-job runs as user and reads both queue.dat and spool/job-0142.json, tips-api listens on port 8080, tips-client holds one connection to it, tips-archiver writes site.tar, and tips-tmp wrote a five-megabyte scratch.dat and then deleted it while still holding it open. /srv/cache is a 20M tmpfs of its own, so a question about a whole filesystem lands on that rather than on the container's root. scratch.dat is missing from the listing below because it has no name left.

/srv/cache:
site.tar

/srv/tips:
app.log
queue.dat
spool

/srv/tips/spool:
job-0142.json
48 outputs, collapsed by default

What has this file open

Give lsof a path and it names every process holding that file. The FD column says which descriptor and in which mode: 3w is descriptor 3 open for writing, 3r for reading. DEVICE, SIZE/OFF and NODE describe the file itself rather than the process.

Find out what is writing to a log file

lsof /srv/tips/app.log

A single writer, on descriptor 3. This is the question lsof exists for, and a path is all it needs.

Show output

Your output will differ: the pid, the device number and the inode are this machine's

COMMAND  PID USER FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-log 152 root 3w   REG   0,53       15 460592 /srv/tips/app.log

Ask about several files at once

lsof /srv/tips/app.log /srv/tips/queue.dat

Paths accumulate, and the answer is one table covering all of them. The mode letters differ here because one process is appending and the other only reading.

Show output

Your output will differ: the pids, the device number and the inodes are this machine's

COMMAND  PID USER FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-log 152 root 3w   REG   0,53       15 460592 /srv/tips/app.log
tips-job 182 user 3r   REG   0,53     4096 460590 /srv/tips/queue.dat

Check whether anything has a file open at all

lsof /etc/passwd; echo "exit $?"

Nothing is holding it, so lsof prints nothing and exits 1. A script can read that status rather than testing whether the output came back empty.

Show output
exit 1

Get just the process ids

lsof -t /srv/tips/app.log

-t drops the table and prints one pid per line, in the form kill and xargs want. It is the only output mode that suppresses the header.

Show output

Your output will differ: the pid is this machine's

152

Count how many processes hold a file

lsof -t /srv/tips/app.log | wc -l

A count is often the whole answer: one writer is a service doing its job, and several are a problem. Without -t the table prints a line per descriptor, so the count would be of descriptors rather than of processes.

Show output
1

Test a file before touching it in a script

for f in /srv/tips/queue.dat /etc/passwd; do
  if lsof -t "$f" >/dev/null; then echo "$f: in use"; else echo "$f: free"; fi
done

The exit status carries the answer, so the output can go to /dev/null. Worth doing before a rotate, a truncate or a mv that would otherwise land on a file something is mid-write on.

Show output
/srv/tips/queue.dat: in use
/etc/passwd: free

Name the command without reading a table

lsof -F c /srv/tips/app.log | grep ^c

-F prints one field per line with a letter saying which field it is, so c is the command name. A pid line comes with it whether you asked or not, which is why the grep is there.

Show output
ctips-log

Narrowing, and the -a that is not optional

-c, -u, -p, -i and -d all select, and they combine with OR unless -a is present. Two selectors without -a widen the answer rather than narrowing it.

Select by command name

lsof -a -c tips-log -d 3

-c matches the start of the command name, and -d 3 limits the answer to descriptor 3. Both conditions apply because -a is there.

Show output

Your output will differ: the pid, the device number and the inode are this machine's

COMMAND  PID USER FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-log 152 root 3w   REG   0,53       15 460592 /srv/tips/app.log

See two selectors combine as OR

lsof -c tips-log -u user | awk 'NR>1 {print $1}' | sort -u

The command asks for a process named tips-log and for files owned by user, and gets both sets: tips-job runs as user and is nothing to do with tips-log. Reading this as a two-part filter is how a lsof -c sshd -u root comes back with the whole machine in it.

Show output
tips-job
tips-log

Turn the same two selectors into AND

lsof -a -c tips-log -u user; echo "exit $?"

-a makes the conditions apply together, and no process called tips-log runs as user, so the answer is empty.

Show output
exit 1

List what one user has open

lsof -a -u user -d 3,4

-d takes a list, so 3,4 covers both of this process's data descriptors. Without -a the -d would apply to every process on the machine as well.

Show output

Your output will differ: the pid, the device number and the inodes are this machine's

COMMAND  PID USER FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-job 182 user 3r   REG   0,53     4096 460590 /srv/tips/queue.dat
tips-job 182 user 4r   REG   0,53       32 460591 /srv/tips/spool/job-0142.json

Exclude a user instead of selecting one

lsof -a -u ^root -d 3

A ^ in front of the value negates it. On a real machine -u ^root is the quickest way to see what the service accounts are doing without wading through everything root has open.

Show output

Your output will differ: the pid, the device number and the inode are this machine's

COMMAND  PID USER FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-job 182 user 3r   REG   0,53     4096 460590 /srv/tips/queue.dat

Ask about two command names

lsof -a -c tips-log -c tips-api -d 3

Repeating a selector ORs it with itself, which is the behaviour you want here: either command, and descriptor 3. The -a applies between the different kinds of selector, not within one kind.

Show output

Your output will differ: the pids, the device numbers and the inodes are this machine's

COMMAND  PID USER FD   TYPE  DEVICE SIZE/OFF   NODE NAME
tips-log 152 root 3w   REG    0,53       15 460592 /srv/tips/app.log
tips-api 157 root 3u  IPv4 2527152      0t0    TCP *:http-alt (LISTEN)

Exclude a command and search a tree

lsof -a -c ^tips-log -d 3,4 +D /srv/tips

Not tips-log, descriptor 3 or 4, and somewhere under /srv/tips, all applying together. Useful when one noisy service is drowning out whatever else is in a directory.

Show output

Your output will differ: the pid, the device number and the inodes are this machine's

COMMAND  PID USER FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-job 182 user 3r   REG   0,53     4096 460590 /srv/tips/queue.dat
tips-job 182 user 4r   REG   0,53       32 460591 /srv/tips/spool/job-0142.json

Match a prefix across several services

lsof -a -c tips- -d 3,4

-c matches from the beginning of the name rather than the whole of it, so one selector covers a family of processes named alike. The TYPE column changes with what is open: REG for a file, IPv4 for a socket.

Show output

Your output will differ: the pids, the device numbers, the inodes and the socket ids are this machine's

COMMAND   PID USER FD   TYPE  DEVICE SIZE/OFF   NODE NAME
tips-log  152 root 3w   REG    0,53       15 460592 /srv/tips/app.log
tips-api  157 root 3u  IPv4 2527152      0t0    TCP *:http-alt (LISTEN)
tips-tmp  164 root 3w   REG    0,55  5242880      2 /srv/cache/scratch.dat (deleted)
tips-arch 173 root 3w   REG    0,55  1048576      3 /srv/cache/site.tar
tips-job  182 user 3r   REG    0,53     4096 460590 /srv/tips/queue.dat
tips-job  182 user 4r   REG    0,53       32 460591 /srv/tips/spool/job-0142.json
tips-clie 187 root 3u  IPv4 2528819      0t0    TCP localhost:45400->localhost:http-alt (ESTABLISHED)

Look at everything one process has open

lsof -a -p "$(lsof -t -a -i :8080 -s TCP:LISTEN)" -d ^mem

Find the pid with one lsof, then hand it to -p in another. -d ^mem drops the shared libraries, which would otherwise be most of the answer.

Show output

Your output will differ: the pid, the device numbers, the inodes and the Python version are this machine's

COMMAND  PID USER  FD   TYPE  DEVICE SIZE/OFF   NODE NAME
tips-api 157 root cwd    DIR    0,53     4096 460585 /srv/tips
tips-api 157 root rtd    DIR    0,53     4096 460534 /
tips-api 157 root txt    REG    0,53  6673736 442800 /usr/bin/python3.13
tips-api 157 root   0r   CHR     1,3      0t0     61 /dev/null
tips-api 157 root   1w   CHR     1,3      0t0     61 /dev/null
tips-api 157 root   2w   CHR     1,3      0t0     61 /dev/null
tips-api 157 root   3u  IPv4 2527152      0t0    TCP *:http-alt (LISTEN)

Reading the FD column

FD holds a descriptor number for most entries and a name for four of them. cwd is the process's working directory, rtd its root directory, txt the executable it is running, and mem a file mapped into its address space. A number arrives followed by r, w or u for read, write or both.

See the whole picture for one process

lsof -a -c tips-job -d ^mem

The three standard descriptors are there as 0, 1 and 2, pointing at /dev/null because nothing attached a terminal. cwd and txt answer where a process thinks it is and which binary it is really running.

Show output

Your output will differ: the pid, the device number, the inodes and the Python version are this machine's

COMMAND  PID USER  FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-job 182 user cwd    DIR   0,53     4096 460585 /srv/tips
tips-job 182 user rtd    DIR   0,53     4096 460534 /
tips-job 182 user txt    REG   0,53  6673736 442800 /usr/bin/python3.13
tips-job 182 user   0r   CHR    1,3      0t0     61 /dev/null
tips-job 182 user   1w   CHR    1,3      0t0     61 /dev/null
tips-job 182 user   2w   CHR    1,3      0t0     61 /dev/null
tips-job 182 user   3r   REG   0,53     4096 460590 /srv/tips/queue.dat
tips-job 182 user   4r   REG   0,53       32 460591 /srv/tips/spool/job-0142.json

Find a process's working directory

lsof -a -c tips-log -d cwd

A service started from the wrong place writes its relative paths somewhere nobody is looking, and cwd is how you confirm it. TYPE is DIR rather than REG, since a directory is a file too.

Show output

Your output will differ: the pid, the device number and the inode are this machine's

COMMAND  PID USER  FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-log 152 root cwd    DIR   0,53     4096 460585 /srv/tips

Find the binary a process is running

lsof -a -c tips-log -d txt

txt is the executable, read from the kernel rather than from the command line, so it survives a process that was started through a wrapper or renamed itself. Here it is the interpreter, because the script has a #! line.

Show output

Your output will differ: the pid, the device number, the inode and the Python version are this machine's

COMMAND  PID USER  FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-log 152 root txt    REG   0,53  6673736 442800 /usr/bin/python3.13

Look at one of the mapped files

lsof -a -c tips-log -d mem | grep locale

mem entries are the shared libraries and data files the loader mapped in, and there are usually a dozen or more per process. A bare lsof scrolls mostly because of them, which is what -d ^mem removes.

Show output

Your output will differ: the pid, the device number, the inode and the size of the locale data are this machine's

tips-log 152 root mem    REG   0,53   367708 188274 /usr/lib/locale/C.utf8/LC_CTYPE

See where stdin, stdout and stderr are pointing

lsof -a -c tips-log -d 0,1,2

Descriptors 0, 1 and 2 are the standard streams, and their modes read the way you would expect: 0r for input, 1w and 2w for output. A daemon whose output vanished is one whose 1 and 2 point at /dev/null.

Show output

Your output will differ: the pid and the device number are this machine's

COMMAND  PID USER FD   TYPE DEVICE SIZE/OFF NODE NAME
tips-log 152 root 0r   CHR    1,3      0t0   61 /dev/null
tips-log 152 root 1w   CHR    1,3      0t0   61 /dev/null
tips-log 152 root 2w   CHR    1,3      0t0   61 /dev/null

Tell a reader from a writer

lsof -a -d 3 +d /srv/tips

Same descriptor number on both processes, different mode letters. A file with two writers is worth investigating, whereas a file with one writer and several readers is a log.

Show output

Your output will differ: the pids, the device number and the inodes are this machine's

COMMAND  PID USER FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-log 152 root 3w   REG   0,53       15 460592 /srv/tips/app.log
tips-job 182 user 3r   REG   0,53     4096 460590 /srv/tips/queue.dat

Watch a long command name get cut off

lsof -a -c tips-arch -d cwd,3

The COMMAND column is nine characters wide, so tips-archiver arrives as tips-arch. The selector has to match the truncated form too, which is why -c tips-arch finds it at all.

Show output

Your output will differ: the pid, the device number and the inodes are this machine's

COMMAND   PID USER  FD   TYPE DEVICE SIZE/OFF NODE NAME
tips-arch 173 root cwd    DIR   0,55       60    1 /srv/cache
tips-arch 173 root   3w   REG   0,55  1048576    3 /srv/cache/site.tar

Print the full command name

lsof +c 0 -a -c tips-arch -d cwd,3

+c sets the column width and 0 means no limit. Worth having in any output you intend to read by name rather than by pid, since two services can easily share their first nine characters.

Show output

Your output will differ: the pid, the device number and the inodes are this machine's

COMMAND       PID USER  FD   TYPE DEVICE SIZE/OFF NODE NAME
tips-archiver 173 root cwd    DIR   0,55       60    1 /srv/cache
tips-archiver 173 root   3w   REG   0,55  1048576    3 /srv/cache/site.tar

A directory, and a filesystem

+d looks at one directory, +D descends the whole tree below it. Both take the path as the thing to search rather than the thing to report on, so they find files by where they live rather than by which process opened them.

List open files in one directory

lsof +d /srv/tips

spool/job-0142.json is open and missing from this answer, because +d stops at the top level. The cwd rows appear because a working directory is an open file like any other.

Show output

Your output will differ: the pids, the device number and the inodes are this machine's

COMMAND   PID USER  FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-log  152 root cwd    DIR   0,53     4096 460585 /srv/tips
tips-log  152 root   3w   REG   0,53       15 460592 /srv/tips/app.log
tips-api  157 root cwd    DIR   0,53     4096 460585 /srv/tips
tips-job  182 user cwd    DIR   0,53     4096 460585 /srv/tips
tips-job  182 user   3r   REG   0,53     4096 460590 /srv/tips/queue.dat
tips-clie 187 root cwd    DIR   0,53     4096 460585 /srv/tips

Search a whole tree

lsof +D /srv/tips

The capital letter descends, and spool/job-0142.json now appears. +D walks the tree itself, so it costs more on a large one and needs permission to read every directory in it.

Show output

Your output will differ: the pids, the device number and the inodes are this machine's

COMMAND   PID USER  FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-log  152 root cwd    DIR   0,53     4096 460585 /srv/tips
tips-log  152 root   3w   REG   0,53       15 460592 /srv/tips/app.log
tips-api  157 root cwd    DIR   0,53     4096 460585 /srv/tips
tips-job  182 user cwd    DIR   0,53     4096 460585 /srv/tips
tips-job  182 user   3r   REG   0,53     4096 460590 /srv/tips/queue.dat
tips-job  182 user   4r   REG   0,53       32 460591 /srv/tips/spool/job-0142.json
tips-clie 187 root cwd    DIR   0,53     4096 460585 /srv/tips

Notice that the search stops at a mount point

lsof +D /srv

/srv/cache is a separate filesystem with two processes working in it, and none of them is here. The walk stops where the mount begins, so a +D / answers about one filesystem rather than about the machine.

Show output

Your output will differ: the pids, the device number and the inodes are this machine's

COMMAND   PID USER  FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-log  152 root cwd    DIR   0,53     4096 460585 /srv/tips
tips-log  152 root   3w   REG   0,53       15 460592 /srv/tips/app.log
tips-api  157 root cwd    DIR   0,53     4096 460585 /srv/tips
tips-job  182 user cwd    DIR   0,53     4096 460585 /srv/tips
tips-job  182 user   3r   REG   0,53     4096 460590 /srv/tips/queue.dat
tips-job  182 user   4r   REG   0,53       32 460591 /srv/tips/spool/job-0142.json
tips-clie 187 root cwd    DIR   0,53     4096 460585 /srv/tips

Let the search cross mount points

lsof -x f +D /srv

-x f permits the walk to follow mounts, and the two processes in /srv/cache appear. It has to come before +D on the command line, and -x l does the same for symbolic links.

Show output

Your output will differ: the pids, the device numbers and the inodes are this machine's

COMMAND   PID USER  FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-log  152 root cwd    DIR   0,53     4096 460585 /srv/tips
tips-log  152 root   3w   REG   0,53       15 460592 /srv/tips/app.log
tips-api  157 root cwd    DIR   0,53     4096 460585 /srv/tips
tips-tmp  164 root cwd    DIR   0,55       60      1 /srv/cache
tips-arch 173 root cwd    DIR   0,55       60      1 /srv/cache
tips-arch 173 root   3w   REG   0,55  1048576      3 /srv/cache/site.tar
tips-job  182 user cwd    DIR   0,53     4096 460585 /srv/tips
tips-job  182 user   3r   REG   0,53     4096 460590 /srv/tips/queue.dat
tips-job  182 user   4r   REG   0,53       32 460591 /srv/tips/spool/job-0142.json
tips-clie 187 root cwd    DIR   0,53     4096 460585 /srv/tips

Ask about a mount point instead of a directory

lsof /srv/cache

A path on its own reports on the filesystem mounted there, rather than walking it, which is the form to use before an umount. It finds the deleted file that +D cannot, since that file no longer has a name to walk to.

Show output

Your output will differ: the pids, the device number and the inodes are this machine's

COMMAND   PID USER  FD   TYPE DEVICE SIZE/OFF NODE NAME
tips-tmp  164 root cwd    DIR   0,55       60    1 /srv/cache
tips-tmp  164 root   3w   REG   0,55  5242880    2 /srv/cache/scratch.dat (deleted)
tips-arch 173 root cwd    DIR   0,55       60    1 /srv/cache
tips-arch 173 root   3w   REG   0,55  1048576    3 /srv/cache/site.tar

Walk the same directory and lose a file

lsof +D /srv/cache

The same four rows minus one: scratch.dat was deleted while open, so nothing in the directory leads to it. Anything that answers by walking names will miss it.

Show output

Your output will differ: the pids, the device number and the inodes are this machine's

COMMAND   PID USER  FD   TYPE DEVICE SIZE/OFF NODE NAME
tips-tmp  164 root cwd    DIR   0,55       60    1 /srv/cache
tips-arch 173 root cwd    DIR   0,55       60    1 /srv/cache
tips-arch 173 root   3w   REG   0,55  1048576    3 /srv/cache/site.tar

A deleted file that is still open

Deleting a file removes its name. The blocks stay allocated until the last process holding it closes it, so df keeps counting them while du and ls cannot see them. This is the usual reason a filesystem stays full after a large file has been removed.

See df and du disagree

df -h /srv/cache; du -sh /srv/cache

The filesystem reports six megabytes in use; walking the directory finds one. The five between them belong to a file with no name.

Show output
Filesystem      Size  Used Avail Use% Mounted on
tmpfs            20M  6.0M   14M  30% /srv/cache
1.0M	/srv/cache

Confirm there is nothing there to delete

ls /srv/cache

site.tar accounts for the megabyte du reported, and nothing you can remove will recover the other five. This is the point at which most people start restarting services at random.

Show output
site.tar

Find every deleted file still held open

lsof -nP +L1

+L1 selects files whose link count is below 1, which is exactly what an unlinked-but-open file is. Run without a path it covers the whole machine, and on a server that is usually a short list worth reading in full.

Show output

Your output will differ: the pid, the device number and the inode are this machine's

COMMAND  PID USER FD   TYPE DEVICE SIZE/OFF NLINK NODE NAME
tips-tmp 164 root 3w   REG   0,55  5242880     0    2 /srv/cache/scratch.dat (deleted)

Show the link count alongside the files in one filesystem

lsof +L1 /srv/cache

With a path, +L1 adds the NLINK column to the whole answer rather than filtering by it. 0 marks the deleted file, 1 an ordinary one and 2 a directory, counting its own entry and its child's ...

Show output

Your output will differ: the pids, the device number and the inodes are this machine's

COMMAND   PID USER  FD   TYPE DEVICE SIZE/OFF NLINK NODE NAME
tips-tmp  164 root cwd    DIR   0,55       60     2    1 /srv/cache
tips-tmp  164 root   3w   REG   0,55  5242880     0    2 /srv/cache/scratch.dat (deleted)
tips-arch 173 root cwd    DIR   0,55       60     2    1 /srv/cache
tips-arch 173 root   3w   REG   0,55  1048576     1    3 /srv/cache/site.tar

Recover the space by ending the process holding it

lsof -t -a +L1 /srv/cache | xargs -r kill
sleep 1
df -h /srv/cache | tail -1

The kernel frees the blocks when the last descriptor closes, so the space comes back when the process ends: 6.0M down to 1.0M with nothing deleted. Check what you are about to kill first, because a service that has a scratch file open is a service in the middle of something.

Show output
tmpfs            20M  1.0M   19M   5% /srv/cache

Sockets and ports

-i selects network files. A socket has no path, so the NAME column holds an address instead, and TYPE reads IPv4 or IPv6 rather than REG. Addresses are resolved to names unless -n and -P say otherwise.

Find out what is using a port

lsof -i :8080

The process listening, and the one connected to it. This is the answer to the message about an address already being in use, and worth running before restarting anything.

Show output

Your output will differ: the pids, the socket ids and the client's port are this machine's

COMMAND   PID USER FD   TYPE  DEVICE SIZE/OFF NODE NAME
tips-api  157 root 3u  IPv4 2527152      0t0  TCP *:http-alt (LISTEN)
tips-clie 187 root 3u  IPv4 2528819      0t0  TCP localhost:45400->localhost:http-alt (ESTABLISHED)

Stop lsof translating ports and addresses

lsof -nP -i :8080

The same two sockets with the lookups turned off: -P leaves ports as numbers and -n leaves addresses as addresses. 8080 printing as http-alt comes from /etc/services, and it makes the output harder to match against the port you were asked about.

Show output

Your output will differ: the pids and the socket ids are this machine's

COMMAND   PID USER FD   TYPE  DEVICE SIZE/OFF NODE NAME
tips-api  157 root 3u  IPv4 2531449      0t0  TCP *:8080 (LISTEN)
tips-clie 187 root 3u  IPv4 2527150      0t0  TCP 127.0.0.1:45400->127.0.0.1:8080 (ESTABLISHED)

List every network connection on the machine

lsof -nP -i

-i with nothing after it takes the lot. On a working server this is where you find the connection nobody remembered opening.

Show output

Your output will differ: the pids and the socket ids are this machine's

COMMAND   PID USER FD   TYPE  DEVICE SIZE/OFF NODE NAME
tips-api  157 root 3u  IPv4 2531449      0t0  TCP *:8080 (LISTEN)
tips-clie 187 root 3u  IPv4 2527150      0t0  TCP 127.0.0.1:45400->127.0.0.1:8080 (ESTABLISHED)

Show only what is listening

lsof -nP -i TCP -s TCP:LISTEN

-s filters on the protocol state, and TCP:LISTEN is the one worth knowing by heart: one line per port this machine will accept a connection on.

Show output

Your output will differ: the pid and the socket id are this machine's

COMMAND  PID USER FD   TYPE  DEVICE SIZE/OFF NODE NAME
tips-api 157 root 3u  IPv4 2531449      0t0  TCP *:8080 (LISTEN)

Show only the established connections

lsof -nP -i TCP -s TCP:ESTABLISHED

TCP:ESTABLISHED asks the question a restart depends on, since these are the clients that will notice one.

Show output

Your output will differ: the pid and the socket id are this machine's

COMMAND   PID USER FD   TYPE  DEVICE SIZE/OFF NODE NAME
tips-clie 187 root 3u  IPv4 2527150      0t0  TCP 127.0.0.1:45400->127.0.0.1:8080 (ESTABLISHED)

Select by address as well as port

lsof -nP -i@127.0.0.1

-i@host narrows to one address, with -i@host:port narrowing further. The listener bound to * is absent because it is not bound to that address, which is the distinction between a service anyone can reach and one only this machine can.

Show output

Your output will differ: the pid and the socket id are this machine's

COMMAND   PID USER FD   TYPE  DEVICE SIZE/OFF NODE NAME
tips-clie 187 root 3u  IPv4 2528819      0t0  TCP 127.0.0.1:45400->127.0.0.1:8080 (ESTABLISHED)

Check whether a port is free

lsof -i :22; echo "exit $?"

Nothing is bound to 22 here, so there is no output and the exit status is 1. A script that wants to pick a free port can loop on this rather than binding and catching the failure.

Show output
exit 1

Count what is attached to a port

lsof -t -i :8080 | wc -l

The listener plus one client. A count like this is what a health check wants, since the number rather than the names says whether anything is connected.

Show output
2

Name the listener without its clients

lsof -F c -a -i :8080 -s TCP:LISTEN | grep ^c

-s TCP:LISTEN drops the clients, leaving the service, and -F c then prints its command name with nothing around it. Swap the -F c for -t when you want the pid instead.

Show output
ctips-api

In a script

-t and -F are the two output modes meant for other programs. The exit status is the third, and 1 means nothing matched rather than something went wrong.

Kill whatever is holding a file

lsof -t /srv/cache/site.tar | xargs -r kill
sleep 1
lsof /srv/cache/site.tar; echo "exit $?"

-r on xargs stops an empty list turning into a bare kill, which would report a usage error rather than doing nothing. Read the list before piping it anywhere: this one ends a process that is mid-write, and a kill -9 in the same position would not let it finish.

Show output
exit 1

Print named fields instead of a table

lsof -F pcn -a -c tips-log -d 3

Each field arrives on its own line, prefixed by a letter naming it: p for pid, c for command, n for name. No column widths, no truncation, and nothing to split on.

Show output

Your output will differ: the pid is this machine's

p152
ctips-log
n/srv/tips/app.log

Ask for one field and get two

lsof -F n -a -c tips-log -d 3

The pid line comes back whether it was asked for or not, since it tells a parser which process the lines after it belong to. Filter it out with grep '^n' rather than assuming a fixed line count.

Show output

Your output will differ: the pid is this machine's

p152
n/srv/tips/app.log

Repeat the search on a timer

lsof -r1 /srv/tips/app.log 2>/dev/null | head -6

-r1 re-runs the search every second and separates each pass with a line of equals signs, which is the closest lsof comes to watching something. Without the head it runs until interrupted.

Show output

Your output will differ: the pid, the device number and the inode are this machine's

COMMAND  PID USER FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-log 443 root 3w   REG   0,53       15 460592 /srv/tips/app.log
=======
COMMAND  PID USER FD   TYPE DEVICE SIZE/OFF   NODE NAME
tips-log 443 root 3w   REG   0,53       15 460592 /srv/tips/app.log
=======