Python — OSError: [Errno 24] Too many open files
Trace Python file descriptor leaks with lsof and fd counts, find open calls missing with or close, and switch to context managers everywhere.
Symptoms
- OSError: [Errno 24] Too many open files raises after the process runs for a while, always at an open or socket call
- lsof -p shows thousands of open files, mostly regular files or pipes pointing at the same paths
- The failure count correlates with the number of requests or files processed, not with time alone
Possible causes
- open() calls in loops or request handlers without with blocks or close()
- tempfile objects whose file handle is kept while only the name is used afterward
- subprocess pipes whose stdout and stderr are never read to completion or closed
- An early return or exception path skips the close call that sits at the end of the function
Diagnosis steps
Count descriptors and group them
ls /proc/<pid>/fd | wc -l && lsof -p <pid> | awk '{print $NF}' | sort | uniq -c | sort -rn | headInterpretation
A fd count in the thousands, dominated by repeats of the same few paths, identifies exactly which resource leaks.
Correlate growth with work
watch -n5 "ls /proc/<pid>/fd | wc -l" # while running trafficInterpretation
fd count tracking request count means per-request leakage; growth only over wall time points at timers or logs.
Audit the leak paths in code
grep -Rn "open(" src/ | grep -v "with open" | head -20Interpretation
Every open without a with is a candidate, and ones inside try blocks with early returns are the usual offenders.
Root cause
File descriptors are a finite per-process resource. Each open, pipe, or socket that is never closed consumes one permanently. Under steady traffic the count climbs until the kernel refuses the next open with EMFILE, which Python reports as OSError Errno 24.
Fix
Wrap every file, pipe, and temporary file in a with block so close is structural rather than manual; use contextlib.closing for objects exposing close without context manager support; read and close subprocess pipes deterministically; and run tests with a low RLIMIT_NOFILE to make leaks fail fast.
Risk note
Raising ulimit -n only widens the bucket. The process still leaks and will hit the new ceiling later, with a bigger blast radius.
Verify the fix
Run a fixed number of requests and assert the fd count returns to its pre-run baseline exactly; repeat with a deliberately lowered ulimit to prove the guard fails loudly.
Symptom walkthrough
The errno is precise and the growth pattern confirms it:
OSError: [Errno 24] Too many open files: 'data/2026-10-22.ndjson'
Watch the descriptor directory live while traffic flows:
watch -n5 "ls /proc/$(pgrep -f app)/fd | wc -l"
# 1042 -> 1089 -> 1137 ... <- one per request
Group the handles by path to name the leaking resource directly:
lsof -p $(pgrep -f app) | awk '{print $NF}' | sort | uniq -c | sort -rn
# 489 data/2026-10-22.ndjson
Hundreds of handles to the same file is the receipt: the code opens it per request and closes it on no path.
Fix in practice
Make closing structural with the with statement, which survives exceptions and early returns:
with open(path, encoding="utf-8") as f:
for line in f:
process(line)
# closed here on every path, including raise
For subprocess pipes, consume and close both streams deterministically:
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p:
out, err = p.communicate() # reads to EOF, closes both fds
For objects that only expose close, delegate to contextlib.closing. In tests, lower the ceiling so leaks fail within seconds instead of hours:
import resource
resource.setrlimit(resource.RLIMIT_NOFILE, (256, 256))