A long-running Linux daemon starts logging "Too many open files" after a few days of uptime. Using `ps`, `lsof` and `/proc`, how do you confirm it is leaking file descriptors and find what it is holding open?
answer
- descriptors, not just files
- count the symlinks under /proc
- read the limit the process actually has
- one reading cannot show a trend
- group lsof output by type
basics
~20 sFind the PID, count entries in /proc/<pid>/fd, and compare that against the limit in /proc/<pid>/limits. Sample the count repeatedly: a steadily rising number is a leak. Then group lsof -nP -p <pid> output by type to see what is accumulating.
solid answer
~50 sFirst get the PID with `pgrep -af`, then count the real descriptors with `ls /proc/<pid>/fd | wc -l` and read the ceiling from `/proc/<pid>/limits`, which shows the soft and hard values actually in force for that running process — not whatever my shell's `ulimit` says. One reading only tells me how close it is; I take the count every minute for a while, because a leak is a monotonically rising line while a busy service plateaus. Once it is confirmed, `lsof -nP -p <pid>` tells me what is accumulating: sockets to one endpoint that are never closed, log files reopened on every rotation, deleted files still held, or inotify watches. I use `-n` and `-P` deliberately so lsof does not do DNS and service lookups on a box that is already unhappy. The fix is in the application; raising the limit only buys time.
code
bash · 3 linespid=$(pgrep -x mydaemon)
ls /proc/$pid/fd | wc -l
grep 'Max open files' /proc/$pid/limitsgo deeper
Know that sockets and pipes consume file descriptors too, and that /proc/<pid>/fd lists what a process currently has open. Be able to find a PID and count its entries.
Be able to compare the count against the limit the process actually holds in /proc/<pid>/limits rather than your shell's, and explain why lsof output lines are not one-to-one with descriptors.
Show the diagnostic method: sample the count over time to separate a leak from a load level, capture the lsof classification before restarting, and read the dominant TYPE to name the bug in the application.
Frame descriptor exhaustion as a class of slow-burn failure: decide what the fleet's defaults should be, where the monotonic-growth signal is watched, and why raising limits is a stopgap you accept only with a deadline attached.
## The error names the resource "Too many open files" is the userspace text for `EMFILE`: the process tried to obtain a file descriptor and had hit its own ceiling. Descriptors are not just files — every open socket, pipe, epoll instance, timer, eventfd and inotify instance consumes one. The investigation is therefore about counting and classifying descriptors, and the whole thing is visible through `/proc`. ## Step 1: count what is actually open `/proc/<pid>/fd` is a directory of symlinks, one per open descriptor, named by descriptor number and pointing at what it refers to. Counting the entries is the authoritative count: ``` pid=$(pgrep -x mydaemon) ls /proc/$pid/fd | wc -l ``` A very common mistake is to use `lsof -p $pid | wc -l` for this. That number is larger, and not by a constant: `lsof` prints a header line, and its `FD` column includes entries that are not descriptors at all — `cwd` (current directory), `rtd` (root directory), `txt` (the executable's own text mapping) and one `mem` line for every memory-mapped library. On a process linked against a dozen shared libraries the inflation is significant. Use `/proc/<pid>/fd` when you want the count; use `lsof` when you want to know what the descriptors *are*. ## Step 2: find the ceiling that applies ``` grep 'Max open files' /proc/$pid/limits ``` This reads the soft and hard limits in force for that specific process. It matters that you read it from `/proc` rather than typing `ulimit -n` in your shell: your shell's limits are its own, and a daemon started by a service manager typically inherits limits from the manager's configuration, not from any login session. `prlimit --pid $pid` prints the same information, and can raise a running process's limit in place when you need to buy time before a restart. Compare the count with the soft limit. Sitting at 1021 out of 1024 is the smoking gun; sitting at 300 out of 65536 means the error came from somewhere else — including a **system-wide** ceiling rather than a per-process one, which is a different diagnosis and shows up in `/proc/sys/fs/file-nr` and `/proc/sys/fs/file-max`. ## Step 3: distinguish a leak from a load level One reading cannot tell you whether descriptors are leaking. Sample it: ``` while sleep 60; do printf '%s %s\n' "$(date +%T)" "$(ls /proc/$pid/fd | wc -l)"; done ``` - A **leak** rises monotonically and does not fall when traffic drops. Uptime correlates better with the count than request rate does. - A **sizing problem** rises and falls with load and plateaus below the limit — the process is behaving correctly and the limit is simply too low for the concurrency you are asking of it. That distinction decides whether you file a bug or change a configuration, so it is the part interviewers most want to hear. ## Step 4: classify what is being held Now `lsof` earns its keep: ``` sudo lsof -nP -p $pid ``` `-n` suppresses reverse DNS on socket addresses and `-P` suppresses port-name lookup; both are network round trips you do not want on a struggling host, and without them lsof can hang for a long time. Read the `TYPE` and `NAME` columns and look for a dominant group: - Hundreds of `IPv4`/`TCP` entries to the same peer with a state like `CLOSE_WAIT` — the application is not closing connections it has finished with, or a connection pool is unbounded. - The same regular file open many times — a handle reopened per request or per rotation. - Entries whose name ends in `(deleted)` — the file was unlinked but the descriptor keeps it alive, which also means its disk space is not returned. - `a_inode` entries backed by inotify or eventfd — a watcher or event loop that creates and never destroys. Because `/proc/<pid>/fd` symlinks are readable directly, `ls -l /proc/$pid/fd | awk '{print $NF}' | sort | uniq -c | sort -rn | head` gives the same grouping without lsof at all, which is useful on a minimal image where lsof is not installed. One access note: reading another user's `/proc/<pid>/fd`, or listing their files with `lsof`, needs privilege. An unprivileged `lsof` silently shows only what you own, which has fooled plenty of people into thinking a process holds almost nothing. ## What to do about it Raising the limit — in the service manager's unit configuration for a permanent change, or with `prlimit` on the running process for an emergency — stops the bleeding but does not fix a leak; a monotonic curve will simply take longer to reach the new ceiling. Restarting resets the count and destroys the evidence, so capture the `lsof` output *before* you restart. The durable fix is in the application: close what you open, bound the pool, and reuse handles rather than reopening them.
- Why is `lsof -p <pid> | wc -l` the wrong way to count a process's open descriptors?Because that output is not one line per descriptor. It includes a header row and rows whose FD column is cwd, rtd, txt or mem — the working directory, root directory, executable text and every memory-mapped library. On a process with many shared libraries that inflates the count substantially and inconsistently. Counting the symlinks in /proc/<pid>/fd gives the real number.
- The count is 300 against a limit of 65536, yet the process still reports too many open files. Where do you look next?At the system-wide ceiling rather than the per-process one. The kernel tracks total allocated file handles and a global maximum, readable under /proc/sys/fs, and exhausting that produces the same error for a process nowhere near its own limit. It is also worth checking whether the limit you read applies to the process that actually failed, since a child may have been started under different limits.
- You see hundreds of sockets in CLOSE_WAIT held by the process. What does that tell you?That the peer closed the connection and the local application never called close on its side. The kernel cannot release the descriptor until the application does, so they accumulate indefinitely. It is an application bug, not a tuning problem — usually an error path that returns without closing, or a client object that is created per request and never released. No amount of raising the descriptor limit will fix it.
saying these in an interview costs you the question
- Counts lines of lsof output as the descriptor count
- Trusts the shell's ulimit as the daemon's limit
- Declares a leak from a single reading
- Restarts the process before capturing evidence
- Raises the limit and calls the leak fixed