Home › DevOps › Too Many Open Files - Raise Limits and Fix Leaks
Intermediate 5 min · September 23, 2026

Too Many Open Files - Raise Limits and Fix Leaks

Raise ulimit -n, set limits.conf and systemd LimitNOFILE, then hunt leaks with lsof -p.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
✓ Production
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 9 min
  • ✓Basic Linux processes and shells
  • ✓Root or sudo for limit changes
  • ✓A service you can restart safely
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • Too many open files (EMFILE/ENFILE) means a process or the system hit its file descriptor cap
  • Check now with ulimit -n, ulimit -Hn, and cat /proc/sys/fs/file-nr for system-wide use
  • Raise safely via /etc/security/limits.conf for logins and LimitNOFILE in the systemd unit for services
  • Hunt leaks with lsof -p PID and ls -l /proc/PID/fd; sockets count too, so check ss -s totals and CLOSE_WAIT piles per peer
✦ Definition~90s read
What is Too Many Open Files Fix?

A file descriptor is a small integer the kernel hands a process for each open file, socket, pipe, or epoll set. When code opens /var/log/app.log it might get fd 12; when it accepts a TCP connection it might get fd 43. The process uses that number for all later reads and writes, and returns it with close when done.

★
Every program gets a box of numbered tickets, and each open file or network connection takes one ticket.

Descriptors are per-process, counted separately for each PID, with a system-wide ceiling on top.

Two errors signal exhaustion. EMFILE means one process hit its own cap: accept returns -1, open fails, and logs fill with Too many open files. ENFILE means the whole system hit fs.file-max, which is rarer and far more serious since every process suffers.

Per-process caps come from three layers that override each other: the shell ulimit (soft and hard), PAM limits.conf for login sessions, and the systemd unit LimitNOFILE for services. Systemd ignores limits.conf for services, which is why raising limits in the terminal never fixes a daemon.

Sockets count because a TCP connection is a file descriptor like any other. A web server holding 900 idle keepalive connections plus 200 open log and library files sits at 1,100 against a 1,024 cap and starts refusing work. Leaks make it worse: a forgotten close in an error path drips a few fds per request until the ceiling hits at peak traffic. The fix pairs bigger boxes with leak hunting.

Plain-English First

Every program gets a box of numbered tickets, and each open file or network connection takes one ticket. Too many open files means the box is empty: no ticket, no new file, no new connection. You can get a bigger box by raising the limit, but if the program keeps dropping tickets under the desk (a leak), the bigger box just delays the same crash. The real fix is twofold: hand out a sensibly bigger box, then find who keeps losing tickets.

Your app starts refusing connections and the logs say Too many open files. Restarts help for a day, then it returns. The cause is almost never disk space: it is the file descriptor limit. Every open file, socket, and pipe consumes one descriptor, and each process has a cap set by ulimit -n. Busy servers with connection churn hit the default 1024 fast.

The trap is raising the limit without asking why usage climbs. A leak that opens sockets and never closes them will eat 65,000 descriptors as happily as 1,024. Blindly doubling limits buys time while hiding the bug. Production-worthy fixes do both: set correct limits at every layer, then prove with lsof that descriptor counts stay flat under load.

This guide walks the full stack: what descriptors are, soft vs hard ulimit, limits.conf for login shells, systemd LimitNOFILE overrides that most teams miss, leak hunting with lsof -p and /proc/PID/fd, and why sockets count against the same budget. You will leave with persistent limits and a leak-hunt routine.

File Descriptors Explained - Files, Sockets, and Pipes

A file descriptor is the kernel ticket for anything a process has open: regular files, TCP and Unix sockets, pipes, eventpoll sets, even /dev/null. open, socket, accept, and pipe each consume one integer slot in the process table. close returns it. The per-process count lives in /proc/PID/fd, one symlink per live descriptor, and the ceiling lives in /proc/PID/limits under Max open files. When the count reaches the ceiling, the next open fails with EMFILE and your logs say Too many open files. Nothing about disk space matters here: a terabyte of free disk does not grant one more descriptor. Two ceilings stack. The per-process soft limit is what the app hits day to day; the hard limit caps how high the soft one can be raised without root. Above both sits fs.file-max, the system-wide budget shared by every process. Hitting file-max prints VFS: file-max limit reached in dmesg and breaks all processes at once. Normal servers never approach it, but containers sharing a host kernel can surprise you. Learn to read the three numbers together: process use from /proc/PID/fd, process ceiling from ulimit -n, system pressure from /proc/sys/fs/file-nr. That trio tells you in seconds whether one app is greedy or the whole box is tired.

fd-check.shBASH
1
2
3
4
5
ulimit -n
ulimit -Hn
cat /proc/sys/fs/file-nr
cat /proc/sys/fs/file-max
ls /proc/$$/fd | wc -l
📊 Production Insight
Dashboards that track only disk and memory miss this failure entirely. A per-process fd graph is the cheapest early warning you can add.
🎯 Key Takeaway
Descriptors cover files, sockets, and pipes per process. Read use, ceiling, and system pressure together before acting.

ulimit Soft vs Hard - Check and Raise Without Lockout

ulimit -n shows the soft limit: the working ceiling your shell and its children obey right now. ulimit -Hn shows the hard limit: the maximum anyone without root can raise the soft one to. Raising soft up to hard needs no privilege: ulimit -n 4096 just works. Raising hard needs root, and lowering hard is one-way for mortals, so be careful in shared sessions. These values are per-process and inherited at fork: changing them in your terminal affects only that shell and what it starts, never a running daemon or a systemd service. That is why ulimit fixes that work in testing vanish in production. Verify the real target instead of your shell: cat /proc/PID/limits shows the service actual soft and hard ceilings. If your shell says 65,535 but the service says 1,024, you fixed the wrong process. For interactive work, set both sensibly in the shell profile. For daemons, note the numbers here and carry them into limits.conf or the systemd unit in the next sections. Never set unlimited in production: one runaway process can then starve the whole host of descriptors instead of failing alone and noisily. Record the chosen soft and hard values in your runbook with the reasoning behind them. The next engineer should see numbers plus justification, not mystery digits.

ulimit-check.shBASH
1
2
3
4
5
ulimit -Sn
ulimit -Hn
cat /proc/1234/limits | grep -i open
ulimit -n 4096
ulimit -Sn
📊 Production Insight
The classic false fix is raising ulimit in a terminal and declaring victory while the daemon keeps its old 1,024 ceiling untouched.
🎯 Key Takeaway
Soft is the working ceiling, hard caps raises. Fix the service process limits, not your shell, and check /proc/PID/limits.

limits.conf and PAM - Make Login Limits Persistent

Shell ulimit changes evaporate on logout, so persistent interactive limits live in /etc/security/limits.conf, enforced by PAM at login. Each line names a domain, a type, an item, and a value: appuser soft nofile 16384 sets the soft ceiling, appuser hard nofile 65536 sets the hard ceiling. The star domain sets defaults for everyone, but prefer named users or groups like @appteam so one change cannot surprise the whole fleet. Two gotchas bite every team once. First, limits.conf applies at session start through pam_limits: existing SSH sessions and running processes keep old values until re-login or restart, so always verify with a fresh login. Second, it does not touch systemd services at all: daemons launched by PID 1 never pass through PAM login, so their limits come from the unit file instead. Syntax errors here fail silently at login, leaving defaults in place with no warning, which is why some raises mysteriously do nothing. Check /etc/pam.d for session required pam_limits.so on custom stacks, keep one root session open while editing, and confirm with ulimit -n after a fresh login before closing out. Store the file in version control or a config manager so diffs show who changed what. Hand-edited hosts drift, and drift is how identical fleets develop mystery differences.

limits-conf.shBASH
1
2
3
4
grep -R pam_limits /etc/pam.d/
cat /etc/security/limits.conf | grep -v '^#' | grep -v '^$'
su - appuser -c 'ulimit -Sn; ulimit -Hn'
cat /proc/1234/limits | grep -i open
📊 Production Insight
Silent limits.conf typos are why raises work on one host and not another. Always verify with a fresh login, never the editing session.
🎯 Key Takeaway
Persist login limits in limits.conf per user or group, ensure pam_limits runs, and verify with a fresh login.

systemd LimitNOFILE - The Override Teams Always Miss

On modern distros systemd owns service limits, and it ignores both shell ulimit and limits.conf for units. A service that needs 16,384 descriptors gets them only from LimitNOFILE in its unit. Check the live value with systemctl show app.service -p LimitNOFILE and the process truth with cat /proc/PID/limits. When those two disagree with your terminal, the unit wins and your shell work was theater. Set it with an override, never by editing vendor units: systemctl edit app.service writes /etc/systemd/system/app.service.d/override.conf where you put LimitNOFILE=16384, then systemctl daemon-reload and systemctl restart. Keep LimitNOFILEInfinity out of production; an uncapped service can exhaust the shared file-max and take neighbors down. Size from data: peak sockets plus files plus 30 percent headroom, rounded up. After restart, confirm the new ceiling in /proc/PID/limits and graph it. If you run containers under systemd, remember Docker and kubelet pass their own defaults down, so check the container runtime docs alongside the unit. Document the chosen number and its math in the unit comment. Add a deploy check that fails if LimitNOFILE is absent from the override. Explicit gates beat tribal knowledge every time the team grows.

systemd-nofile.shBASH
1
2
3
4
5
systemctl show app.service -p LimitNOFILE
systemctl edit app.service
systemctl daemon-reload
systemctl restart app.service
cat /proc/1234/limits | grep -i open
📊 Production Insight
After any limit incident, the first command on the service host should be systemctl show, not ulimit. The unit value is the truth.
🎯 Key Takeaway
Services obey LimitNOFILE only. Override with systemctl edit, reload, restart, and verify in /proc/PID/limits.

Hunt the Leak With lsof - Find Who Holds What

When counts climb without extra traffic, something opens and never closes. lsof -p PID lists every descriptor with its type and target: REG for files, IPv4 or TCP for sockets, FIFO for pipes. Rank suspects across the box with an awk over the PID column, then zoom into the top holder. Read the NAME column like a detective. Hundreds of lines to one log path mean rotation is missing or the app holds deleted files open. Thousands of socket lines to one peer, especially in CLOSE_WAIT, mean retry or close logic is broken. Deleted markers mean disk space will not free until the holder restarts. Confirm with /proc/PID/fd directly: ls -l shows live symlinks, and sampling twice ten minutes apart proves growth versus steady state. For deeper proof, strace -e trace=open,openat,close -p PID for a minute shows opens without matching closes. Fix the code path, not the number: add missing close calls, use try-with-resources or finally blocks, enable log rotation, and cap connection pools. Re-run the same lsof after deploy and demand a flat line under load before closing the ticket. Save the before and after lsof summaries in the incident note. Concrete counts convince reviewers faster than claims that the leak is gone.

lsof-hunt.shBASH
1
2
3
4
lsof -p 1234 | wc -l
lsof -p 1234 | grep -c CLOSE_WAIT
ls -l /proc/1234/fd | head -30
lsof -p 1234 | awk '{print $NF}' | sort | uniq -c | sort -rn | head
📊 Production Insight
CLOSE_WAIT piles to a single peer are the smoking gun of retry-path leaks. Grep lsof for CLOSE_WAIT before anything else.
🎯 Key Takeaway
Rank holders with lsof, sample /proc/PID/fd twice to prove growth, fix the close path, then demand a flat line.

Sockets Count Too - Size for Connections, Not Just Files

Teams size for log files and forget each TCP connection costs a descriptor too. A server with 900 keepalive connections, 200 open files, and a few pipes sits near 1,100 against a 1,024 cap and fails on the next accept. The error still says Too many open files, which sends people hunting logs while sockets are the culprit. Measure both sides: ss -s gives host socket totals by state, lsof -p PID counts the process share, and /proc/PID/fd shows the combined pressure. TIME_WAIT and CLOSE_WAIT deserve special attention. TIME_WAIT is normal TCP hygiene after close and recycles on its own, but CLOSE_WAIT means your app never closed its end and those fds never recycle. Tune keepalive timeouts and pool sizes to match real concurrency, fix retry paths to close on every outcome, and only then size LimitNOFILE with headroom over measured peak. Monitor established plus CLOSE_WAIT per service and alert before the ceiling. Connection churn at peak is when leaks turn from slow drips into outages, so load-test with failure injection and watch the fd line stay flat. Put the socket-plus-file math in the capacity doc next to CPU and memory. Descriptors are capacity too, and they deserve the same review each quarter.

⚠ Do Not Set Unlimited in Production
LimitNOFILE=infinity lets one leaking process exhaust the shared system budget and crash every neighbor. Always set an explicit number with headroom, and alert at 70 percent.
📊 Production Insight
Accept failures blamed on files are usually sockets. Check ss -s alongside lsof whenever the numbers do not add up.
🎯 Key Takeaway
Every connection costs a descriptor. Track sockets per process, kill CLOSE_WAIT leaks, and size limits over measured peak.
● Production incidentPOST-MORTEMseverity: high

Leaked Sockets Crashed Checkout Every Night for a Week

Symptom
Checkout API returned 502s between 20:00 and 21:00 for six straight days. App logs showed Too many open files (EMFILE) on two of eight pods. Restarts cleared it until the next evening peak. CPU and memory were normal, so autoscaling never triggered.
Assumption
The team assumed traffic growth had outrun the 4,096 limit and doubled LimitNOFILE to 8,192. The crash moved one hour later but still happened. They then suspected a slowloris attack and tuned timeouts. Neither addressed the climb because nobody graphed descriptor counts against request counts.
Root cause
The payment client opened a new socket per retry but closed it only on success. Failed retries leaked one fd each, roughly 40 per minute at peak. lsof -p showed thousands of CLOSE_WAIT sockets to the payment host. ls /proc/PID/fd confirmed the count grew linearly with failed payments, matching the evening peak exactly.
Fix
Patched the client to close the socket in a finally block on every path, then set LimitNOFILE to 16,384 with headroom. Added a dashboard on process fd counts plus an alert at 70 percent of the limit. Load-tested with forced payment failures to prove counts stay flat when retries fail.
Key lesson
  • Graph fd counts per process before raising limits: a steady climb under load is a leak, not a small box.
  • Retry paths must close sockets on failure, not just success; CLOSE_WAIT piles are the fingerprint.
  • Alert at 70 percent of LimitNOFILE so leaks page you days before peak traffic crashes.
Production debug guideConfirm the ceiling, find the holder, prove the leak, then persist the fix.5 entries
Symptom · 01
Need to confirm it is really fd exhaustion
→
Fix
Run ulimit -n and ulimit -Hn for the process shell, cat /proc/sys/fs/file-nr for system use versus max, and dmesg | tail -20 for VFS file-max exceeded lines. EMFILE names the process limit; file-max exceeded names the system limit.
Symptom · 02
Must find which process holds the descriptors
→
Fix
Run lsof | awk '{print $2}' | sort | uniq -c | sort -rn | head to rank holders, then lsof -p PID | wc -l and ls /proc/PID/fd | wc -l for the suspect. Compare against cat /proc/PID/limits to see its Max open files ceiling.
Symptom · 03
Need to tell leak from legitimately busy
→
Fix
Run ls -l /proc/PID/fd | head -30 twice, ten minutes apart, and diff the targets. Growth in socket: entries to one peer means a connection leak. Growth in one log path means missing rotation. Flat counts mean the box is just small.
Symptom · 04
Limit change in terminal did not stick for the service
→
Fix
Run systemctl show SERVICE -p LimitNOFILE and cat /proc/PID/limits. Systemd units ignore limits.conf and shell ulimit. Set LimitNOFILE=16384 in the unit override with systemctl edit, then daemon-reload and restart.
Symptom · 05
Sockets suspected of eating the budget
→
Fix
Run ss -s for socket totals and lsof -p PID | grep -c TCP for the process share. Correlate with CLOSE_WAIT counts via ss -o state close-wait. Fix keepalive and retry-close logic, not just the numeric limit.
Open Files Causes Compared
Root CauseHow to ConfirmFixPrevention
Soft limit too smallulimit -n low vs steady use in lsofRaise soft up to hard; persist configShip sensible defaults in IaC images
Leaked sockets or fileslsof grows over time; CLOSE_WAIT pilesFix close path; restart; verify flatFd dashboards; fail-injection load tests
systemd override missingsystemctl show differs from shell ulimitLimitNOFILE in override + reloadReview units in deploy checklists
System-wide exhaustionfile-nr near file-max; dmesg VFS errorsRaise file-max; cap greedy servicesHost-level fd and socket alerts
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
fd-check.shulimit -nFile Descriptors Explained - Files, Sockets, and Pipes
ulimit-check.shulimit -Snulimit Soft vs Hard - Check and Raise Without Lockout
limits-conf.shgrep -R pam_limits /etc/pam.d/limits.conf and PAM - Make Login Limits Persistent
systemd-nofile.shsystemctl show app.service -p LimitNOFILEsystemd LimitNOFILE - The Override Teams Always Miss
lsof-hunt.shlsof -p 1234 | wc -lHunt the Leak With lsof - Find Who Holds What

Key takeaways

1
Too many open files means descriptor exhaustion
check process use, ceiling, and system pressure together.
2
Soft is the working cap, hard bounds raises; services obey systemd LimitNOFILE, not your shell.
3
Persist logins in limits.conf with pam_limits, and services in unit overrides with reload and restart.
4
Prove leaks by sampling lsof twice
growth is a bug, flat is a small box.
5
CLOSE_WAIT piles mean broken retry-close logic; fix code paths, not just numbers.
6
Never use infinity in production; alert at 70 percent of an explicit limit.

Common mistakes to avoid

5 patterns
×

Raising limits without checking for a leak

Symptom
Each raise buys a shorter calm period: a week, then a day, then hours. Descriptor graphs climb steadily between restarts.
Fix
Sample lsof -p and /proc/PID/fd twice before any raise. Flat means small box; climbing means leak. Fix code first.
×

Setting ulimit in a terminal and expecting daemons to follow

Symptom
Shell shows 65,535 but the service still crashes at 1,024. /proc/PID/limits never changed.
Fix
Set LimitNOFILE in the systemd override for services and limits.conf for logins. Verify the process itself, not your shell.
×

Editing vendor unit files directly

Symptom
Package upgrade overwrites the custom LimitNOFILE and the incident returns months later with no config diff in git.
Fix
Use systemctl edit overrides in /etc/systemd/system/...d/. Keep units in version control and review them on deploy.
×

Using infinity for LimitNOFILE

Symptom
One leaking service eats the shared file-max and crashes neighbors. Blast radius jumps from one app to the whole host.
Fix
Set explicit numbers with headroom over measured peak. Alert at 70 percent so leaks page early.
×

Forgetting sockets in capacity math

Symptom
File counts look fine but accepts fail at peak. ss shows thousands of established and CLOSE_WAIT sockets.
Fix
Size limits over peak connections plus files. Fix keepalive, pools, and retry-close paths alongside the number.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What counts as a file descriptor, and how do you see them for a process?
Q02JUNIOR
What is the difference between soft and hard ulimit, and who can raise e...
Q03SENIOR
You raised ulimit in the terminal but the service still crashes. Why?
Q04SENIOR
How do you prove a descriptor leak versus a limit that is just small?
Q05SENIOR
A host hits system-wide ENFILE while per-process limits look fine. What ...
Q01 of 05JUNIOR

What counts as a file descriptor, and how do you see them for a process?

ANSWER
Regular files, sockets, pipes, and epoll sets each take one descriptor. List them with lsof -p PID or ls -l /proc/PID/fd, count with wc -l, and read the ceiling in cat /proc/PID/limits. Sockets count exactly like files.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Is Too many open files about disk space?
02
Why did raising ulimit do nothing for my service?
03
Do network connections really use file descriptors?
04
What does CLOSE_WAIT mean in lsof output?
05
What values should I set for LimitNOFILE?
06
How do I find which process holds the most descriptors?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
✓ Verified
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
🔥

That's Linux. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
No Route to Host Fix
19 / 19 · Linux
Next
Nginx Bind Address in Use Fix
→