Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

Facing “Too Many Open Files” error? File Descriptors Explained

Published

Linux
Facing “Too Many Open Files” error? File Descriptors Explained screenshot 1

Your app runs fine for weeks, then one busy afternoon the logs start bleeding this:

OSError: [Errno 24] Too many open files

Nginx starts refusing connections. Your database driver throws. Uploads fail. Nothing is wrong with your code logic, and the server has plenty of CPU and memory to spare. You have hit a limit most developers never think about until it takes production down: the file descriptor limit. This article explains what file descriptors actually are, why you run out, and then we trigger the error on purpose, watch it happen, and fix it. Every command below was run before publishing.

What a file descriptor actually is

On Linux, the old saying is “everything is a file.” When a process opens something, the kernel hands it back a small integer called a file descriptor, or FD. That process then uses the integer to read and write. The key insight is that “something” is much broader than files on disk. An FD can point to:

  • a regular file
  • a network socket (every open TCP connection is an FD)
  • a pipe between processes
  • an epoll or eventfd instance
  • a device

Every process starts with three FDs already open: 0 is standard input, 1 is standard output, and 2 is standard error. You can see this directly. A bare process holds exactly these:

['0', '1', '2', '3']

(The 3 is a transient descriptor from listing the directory itself.) Every new file you open, every connection you accept, every socket you dial, consumes one more integer from a finite pool. That pool is the thing you run out of.

The limits that bite you

There are two layers of limits, and knowing which one you hit tells you how to fix it.

Per-process limits come in a soft and a hard flavor. The soft limit is the ceiling actually enforced on a process. The hard limit is the maximum the soft limit is allowed to be raised to. Check them with ulimit:

ulimit -Sn   # soft limit
ulimit -Hn   # hard limit

You can also read them straight from the kernel for any process:

cat /proc/self/limits | grep "open files"
Max open files            1024                 524288               files

So this process can open 1024 things at once (soft), and could be raised as high as 524288 (hard) without root.

System-wide limits cap the total FDs across every process on the machine, exposed as fs.file-max and fs.nr_open via sysctl. You rarely hit these before the per-process limit, but on very busy servers they matter too.

Why you actually run out

Two reasons dominate.

The first is simply scale. A busy web server with 10,000 concurrent connections is holding at least 10,000 socket FDs, before it opens a single log file or database connection. The default soft limit of 1024 is laughably low for that. This is why tuning nofile is step one when you put Nginx or a database under real load.

The second, and nastier, reason is a file descriptor leak. Your code opens a file or a connection and never closes it. Each request leaks one FD, the count creeps up over hours or days, and then the server falls over far from the actual bug. A leak looks exactly like scale until you watch the FD count climb with no matching rise in traffic.

Hands-on: trigger it, then fix it

You need Docker. Everything runs in a throwaway container so your own machine’s limits are never touched.

1. See your current limit

docker run --rm python:3.12-slim bash -lc 'echo "soft=$(ulimit -Sn) hard=$(ulimit -Hn)"'
soft=1024 hard=524288

2. Break it on purpose

We will lower the soft limit to 30 and then try to open 100 files. Save this small script:

# openmany.py
import os, sys, tempfile, errno
target = int(sys.argv[1])
files = []
d = tempfile.mkdtemp()
try:
    for i in range(target):
        files.append(open(os.path.join(d, f"f{i}"), "w"))
    print(f"opened all {target} files, soft limit was high enough")
except OSError as e:
    print(f"stopped after {len(files)} files: "
          f"errno={e.errno} ({errno.errorcode[e.errno]}) {e.strerror}")
finally:
    for f in files:
        f.close()

Run it with the soft limit capped at 30:

docker run --rm -v "$PWD/openmany.py:/openmany.py" python:3.12-slim \
  bash -c 'ulimit -Sn 30; python3 /openmany.py 100'
stopped after 27 files: errno=24 (EMFILE) Too many open files

There it is. It stopped at 27, not 30, because the standard streams and a couple of internal descriptors already used three of the budget. EMFILE, errno 24, is the exact error your production app throws. Notice it does not crash randomly, it fails precisely at the ceiling.

3. Fix it by raising the soft limit

Same script, same 100 files, but first raise the soft limit to 500:

docker run --rm -v "$PWD/openmany.py:/openmany.py" python:3.12-slim \
  bash -c 'ulimit -Sn 500; python3 /openmany.py 100'
opened all 100 files, soft limit was high enough

Same code, no error. The only thing that changed was the limit. This is the whole fix for a scale problem: give the process enough descriptors.

4. Count open FDs on a live process

When you suspect a leak, you need to watch the count. Every process exposes its open descriptors under /proc/<pid>/fd:

ls /proc/<pid>/fd | wc -l

For example, a plain sleep process holds just the three standard streams:

sleep process pid=13 holds these fds: 0 1 2
count = 3

Run that wc -l on your app’s PID every few seconds. If the number climbs steadily while traffic is flat, you have a leak, and the entries in /proc/<pid>/fd (which are symlinks to the actual files and sockets) tell you exactly what is being left open. On a machine with lsof installed, lsof -p <pid> gives the same answer with more detail.

Making the fix permanent

Here is the trap that catches everyone: running ulimit -n 500 only changes the current shell and its children. Restart the service and it reverts. To raise the limit for real, set it where the process is actually launched.

For a systemd service (the modern default), set it in the unit file:

[Service]
LimitNOFILE=65536

Then systemctl daemon-reload and restart the service.

For login sessions, edit /etc/security/limits.conf:

appuser  soft  nofile  65536
appuser  hard  nofile  65536

For specific software, many servers have their own knob that must also be raised. Nginx, for example, needs worker_rlimit_nofile in its config, because setting the OS limit alone is not enough if the app caps itself lower.

The rule: the OS soft limit, and any application-level limit, both have to be high enough. The lower of the two wins.

Gotchas worth knowing

  • A non-root process cannot exceed the hard limit. You can raise the soft limit up to the hard limit freely, but going past the hard limit needs privilege. Raise the hard limit in the same config files if you need headroom.
  • Sockets count, and they linger. Connections in TIME_WAIT still hold descriptors for a while after closing. A high connection churn can eat FDs even when nothing looks open.
  • Fix the leak, do not just raise the limit. If the count only ever grows, a bigger limit just delays the crash. Raising nofile is correct for scale, but it is a bandage for a leak.
  • Check the system-wide ceiling too. If fs.file-max is the wall you hit, per-process tuning will not help. Raise it with sysctl.

Wrapping up

“Too many open files” is not a mysterious kernel gremlin. It is a finite pool of small integers, one per open file, socket, or pipe, running dry. Once you can see the limit (ulimit -Sn), watch the usage (/proc/<pid>/fd), and set it where the process actually starts (LimitNOFILE), the error goes from a 2 a.m. panic to a 30-second diagnosis. And if the count only climbs, remember it is probably a leak wearing a scale costume, so find the file or connection your code forgot to close.

Related guides

Account services are unavailable until the production backend is configured.