Huny's Dev Blog
huny.dev

Linux Too Many Open Files: Diagnose EMFILE, FD Leaks, and Process Limits

Distinguish EMFILE from ENFILE and inspect the real process limit. Seven isolated Python checks reproduce FD exhaustion, recovery after close, and exception-safe cleanup.

Hun Jang
Hun Jang Nov 7, 2026

When a Linux server reports Too many open files, first check whether the error is EMFILE, then inspect the failing process's actual FD limit together with changes in its open descriptors. Raising a limit does not fix a missing close. This Python 3 example lowers the limit to 64 in a separate test process to reproduce exhaustion and recovery, then outlines how to distinguish legitimate concurrency from a leak.

Concept illustration of finite process connection slots and releasing one to make room. It does not depict exact FD counts or kernel structures.

Written and documentation checked: November 7, 2026. Verified on Linux with CPython 3.12.14. This is a synthetic test; no user server or real service limits were changed. It is not a Windows example.

Distinguish EMFILE from ENFILE first

Linux open(2) distinguishes EMFILE, a per-process FD limit being reached, from ENFILE, a system-wide open-file limit being reached. Record errno rather than only the error string. Do not treat both errors as the same limit-increase procedure.

Descriptors are not limited to disk files. The /proc/PID/fd documentation also describes sockets, pipes, and entries such as epoll. Observing that an application opens few configuration files does not rule out descriptor exhaustion.

More precisely, the per-process RLIMIT_NOFILE is one greater than the maximum descriptor number that can be opened. It is not simply a limit on distinct file paths. getrlimit(2)

Inspect the failing process, not just your shell

These are read-only diagnostic examples whose PID must be replaced for the target environment. They were not run against the user's processes in this verification. PID 1234 is a placeholder.

Check the soft and hard values of Max open files in limits, then inspect whether fd entries represent regular files, sockets, pipes, or anon_inode objects. A permissions failure or process exit does not mean no descriptors exist. The listing observes a changing process at a moment in time; it is not a fully atomic snapshot.

The shell command ulimit -Sn shows that shell's soft limit. It does not prove that a process already started by a service manager or container has the same limit. Inheritance by children and preservation across exec are documented in the getrlimit(2) NOTES. Verify the actual target process.

Reproduction: lower the limit only in a test child

Save the following as fd_limit_demo.py and run python3 fd_limit_demo.py on Linux. It uses only the standard library. A separate Python child sets its soft limit to 64 while preserving its hard limit. It stops if the initial soft limit is below 64. No system-wide setting or other process limit is changed.

The child opens /dev/null read-only and retains the descriptors. It does not accumulate file contents or create network connections. The Python resource documentation describes the getrlimit and setrlimit interfaces.

Actual output:

What the seven checks establish

Check
Observed result
Interpretation
1. Child limit
soft=64; hard unchanged
Only the test process uses the smaller limit
2. Retained descriptors
A new open fails with EMFILE
A file can exist while no descriptor is available
3. Release one
A new open succeeds and reuses the number
close returned a descriptor slot
4. Release all
fstat on the retained descriptors returns EBADF
The handles retained by this test were closed
5. Sequential repetition
256 context-managed open/close cycles succeed
Total opens differ from simultaneously retained descriptors
6. Exception path
The descriptor closes after an exception inside with
Checking only normal returns is insufficient
7. Parent limit
Unchanged before and after execution
The child's limit change did not propagate to the parent

A limit of 64 does not mean exactly 64 additional files can be opened. Standard input, output, error, and other descriptors already occupy slots. The example makes no fixed claim about the number of new opens before exhaustion. Descriptor numbers can also be reused: a real program must not keep stale numbers and close them later as a cleanup strategy.

Separate leaks from legitimate concurrency before increasing limits

Repeat equivalent load intervals and compare descriptor types and counts. A type that keeps accumulating after requests finish is a clue for tracing ownership and termination paths. Connection pools and intentional caches also retain descriptors, so a high count alone does not establish a leak.

For resources whose lifetime ends with each operation, close them within that scope, as in check 5, and verify exception, timeout, and cancellation paths. For pooled resources, clarify whether the pool or caller owns them and avoid unlimited concurrent work or unlimited pool growth. If legitimate concurrency approaches the limit, adjust concurrency, pool size, and the required limit together, then rerun the same load.

These checks do not test ENFILE, actual socket exhaustion, service-manager or container configuration changes, or prolonged load. The successful with example does not establish that an entire application is leak-free. After a production change, recheck the actual process limit and the FD trend after load subsides.

한국어 버전: Linux Too many open files 해결: EMFILE·FD 누수·프로세스 한도 구분하기Linux Too many open files 해결: EMFILE·FD 누수·프로세스 한도 구분하기

추천 글