Skip to content

Fix: EMFILE Too Many Open Files / ulimit Error on Linux

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix EMFILE too many open files errors on Linux and Node.js — caused by low ulimit file descriptor limits, file handle leaks, and how to increase limits permanently.

The Error

Your application crashes or refuses new connections with:

Error: EMFILE: too many open files, open '/app/data/file.txt'

Or in Node.js:

Error: EMFILE: too many open files, watch
    at FSWatcher.<anonymous> (node:internal/fs/watchers:244:19)

Or from the OS:

bash: /tmp/test.sh: Too many open files
ulimit: open files: cannot modify limit: Operation not permitted

Or in system logs:

kernel: VFS: file-max limit 65536 reached

The process hit the maximum number of file descriptors it is allowed to open simultaneously.

Why This Happens

Every open file, network socket, pipe, anonymous inode, epoll instance, and device on Linux is represented by a file descriptor (FD). The kernel enforces three layers of limits, and a process can be blocked by any of them: the per-process soft limit, the per-process hard limit, and the system-wide cap. When your code calls open(), socket(), or accept() and the limit is hit, glibc returns the EMFILE errno. Node.js, Python, Java, and most other runtimes simply surface that errno as the error you see.

Linux enforces limits on how many FDs a process (and a user) can have open at once:

  • Soft limit (ulimit -n): the current limit enforced for the process. Default is often 1024 on older systems or 65536 on newer ones. A process can raise its own soft limit up to the hard limit without privileges.
  • Hard limit (ulimit -Hn): the maximum the soft limit can be raised to without root privileges. Only CAP_SYS_RESOURCE (or root) can raise it further.
  • System-wide limit (/proc/sys/fs/file-max): the total FDs the kernel allows across all processes. On modern kernels this scales with RAM and is usually large, but it can still be hit by a misbehaving service.

A subtle point that catches many developers: limits are inherited at process start and are not updated when you later change ulimit or /etc/security/limits.conf. The only ways to change a running process’s limits are prlimit(1) from another shell, or restarting the process. This is why a “I already raised the limit” attempt frequently does nothing — the limit you raised applies to the next shell, not to the daemon that started two days ago.

Common causes:

  • Default soft limit too low (1024) for applications that open many files or connections.
  • File descriptor leak — files/sockets opened but never closed.
  • Too many watched files (webpack, Jest, Node.js file watchers).
  • Heavy concurrency — a server handling thousands of simultaneous connections.
  • Docker containers inheriting the host’s limits incorrectly.
  • systemd-managed services ignoring /etc/security/limits.conf entirely (systemd reads its own LimitNOFILE= directive).

Platform and Environment Differences

The defaults for “too many open files” vary wildly depending on where your process is running. Knowing the defaults for each platform saves hours of guessing.

systemd-managed services (most modern Linux distros). systemd does not honor /etc/security/limits.conf for services it starts. Each unit file has its own LimitNOFILE= directive. Recent systemd versions (240+) default to LimitNOFILE=524288 for services, but older systemd shipped with 1024:4096. Check the running value with cat /proc/<PID>/limits — do not trust the shell’s ulimit -n output for a systemd service.

Interactive shells (ulimit -n). The soft limit you see in a login shell comes from /etc/security/limits.conf plus pam_limits.so. On Ubuntu 22.04+ and most modern distros, the soft default is 1024 and the hard is 1048576 (1M). You can raise the soft up to the hard without root.

Docker containers. Containers inherit the dockerd process’s limits unless you pass --ulimit nofile=.... Recent Docker Desktop and most managed runtimes set a higher default, but self-managed dockerd often inherits the shell that started it (1024). Inside a container, ulimit -n reflects the container’s namespace, not the host. Always set --ulimit nofile=65536:65536 for production services.

Kubernetes pods. Pods inherit the container runtime’s defaults. Set securityContext.sysctls for kernel-level tweaks, or use a sidecar/init container to call prlimit. Many K8s users hit this on ingress controllers (nginx ingress, Traefik) under load and need to set the container’s --ulimit via the container runtime config.

Alpine Linux / musl. Alpine ships with ulimit -n of 1024 by default and does not include pam_limits.so (Alpine does not use PAM at all). Editing /etc/security/limits.conf on Alpine does nothing. Use /etc/init.d/ rc service rc_ulimit settings, or set limits in the entrypoint script.

macOS (launchctl maxfiles). macOS uses launchd, not systemd. The per-process limit is set with launchctl limit maxfiles. Default on recent macOS is 256 soft, unlimited hard for interactive shells, but Docker Desktop on macOS sets its own limits inside the Linux VM. Running ulimit -n 65536 in Terminal works for that shell only.

Windows Subsystem for Linux (WSL2). WSL2 honors Linux limits but inherits from /etc/wsl.conf and the parent Windows process. Some Windows AV software can hold FDs on watched files, indirectly tripping EMFILE in Node.js builds.

fs.file-max kernel sysctl. This is the system-wide ceiling across all processes. On a 4 GB box modern kernels set this to ~400000 by default; raising it to 2097152 is safe on production servers. This is the limit you check when cat /proc/sys/fs/file-nr shows open_fds near max_fds.

FD accounting in containers vs cgroups. Containers using cgroups v2 may have an additional pids.max cap; if your “too many open files” coincides with fork failures, suspect the PID cgroup, not nofile.

Fix 1: Increase the Limit for the Current Session

Check and raise the limit immediately (applies to the current shell session only):

# Check current soft limit
ulimit -n

# Check hard limit
ulimit -Hn

# Raise soft limit to hard limit (no root needed)
ulimit -n $(ulimit -Hn)

# Raise to a specific value (must be ≤ hard limit)
ulimit -n 65536

# Verify
ulimit -n

This only affects the current shell and processes started from it. It resets after logout.

Pro Tip: After raising the limit with ulimit -n, restart your application from the same shell session. The process inherits the shell’s limits at the time it starts — changing ulimit after the process is running has no effect on that process.

Fix 2: Increase Limits Permanently for a User

Edit /etc/security/limits.conf to persist the change across reboots and logins:

sudo nano /etc/security/limits.conf

Add these lines (replace ubuntu with your username, or use * for all users):

# /etc/security/limits.conf
ubuntu soft nofile 65536
ubuntu hard nofile 65536

# For all users:
* soft nofile 65536
* hard nofile 65536

# For root (requires separate entry):
root soft nofile 65536
root hard nofile 65536

Apply the change:

Log out and log back in, or start a new session. Verify:

ulimit -n   # Should show 65536

Also edit /etc/pam.d/common-session if limits are not applying:

# Add this line if not present:
session required pam_limits.so

Fix 3: Increase the System-Wide File Descriptor Limit

If many processes are hitting limits simultaneously, raise the kernel’s global cap:

# Check current system-wide limit
cat /proc/sys/fs/file-max

# Check currently open FDs system-wide
cat /proc/sys/fs/file-nr
# Output: open_fds  unused_fds  max_fds

# Temporarily increase (resets on reboot)
sudo sysctl -w fs.file-max=2097152

# Permanently increase — add to /etc/sysctl.conf
echo "fs.file-max = 2097152" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p   # Apply without reboot

Fix 4: Fix Limits for systemd Services

If your application runs as a systemd service, limits.conf does not apply by default — systemd manages its own limits. Edit the service file:

sudo systemctl edit myapp.service

Add:

[Service]
LimitNOFILE=65536

Or edit the service file directly:

sudo nano /etc/systemd/system/myapp.service
[Service]
User=ubuntu
ExecStart=/usr/bin/node /app/server.js
LimitNOFILE=65536
Restart=on-failure

Apply changes:

sudo systemctl daemon-reload
sudo systemctl restart myapp.service

# Verify the limit is applied
cat /proc/$(pgrep -f "node /app/server.js")/limits | grep "open files"

Fix 5: Fix EMFILE in Node.js File Watchers

Node.js uses inotify watches for file watching (fs.watch, webpack HMR, Jest). Each watched file or directory consumes an inotify instance, not just a file descriptor — but there is also an inotify instance limit:

# Check current inotify limits
cat /proc/sys/fs/inotify/max_user_watches    # Default: 8192
cat /proc/sys/fs/inotify/max_user_instances  # Default: 128

# Increase watches (fix for "ENOSPC: System limit for number of file watchers reached")
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

For ENOSPC file watcher errors specifically, see Fix: ENOSPC: System limit for number of file watchers reached.

Reduce the number of files Node.js watches:

In webpack or Jest config, exclude node_modules and other large directories from watching:

// webpack.config.js
module.exports = {
  watchOptions: {
    ignored: /node_modules/,
  },
};
// jest.config.js
module.exports = {
  watchPathIgnorePatterns: ["node_modules", "dist", ".git"],
};

Fix 6: Fix File Descriptor Leaks

If the limit keeps being hit despite raising it, the application may be leaking file descriptors — opening files or connections without closing them:

Check open FDs for a running process:

# Find the PID
pgrep -f "node server.js"

# Count open FDs
ls /proc/<PID>/fd | wc -l

# List open files
lsof -p <PID>

# Watch FD count in real time
watch -n 1 "ls /proc/<PID>/fd | wc -l"

Check which types of FDs are leaking:

lsof -p <PID> | awk '{print $5}' | sort | uniq -c | sort -rn
# REG = regular files
# IPv4/IPv6 = network sockets
# FIFO = pipes

If the count grows continuously, you have a leak. Common Node.js leak patterns:

// Leaked — file opened but never closed
const fd = fs.openSync("data.txt", "r");
// ... forgot fs.closeSync(fd)

// Fixed — use fs.promises with proper cleanup
const fileHandle = await fs.promises.open("data.txt", "r");
try {
  const content = await fileHandle.read(...);
} finally {
  await fileHandle.close(); // Always close in finally
}

// Better — use streams that auto-close
const stream = fs.createReadStream("data.txt");
stream.on("end", () => stream.destroy());

Fix 7: Fix Limits in Docker Containers

Docker containers inherit the host’s ulimit settings by default. If the host limit is low, containers hit it too:

Set limits in docker run:

docker run --ulimit nofile=65536:65536 myapp

Set limits in docker-compose.yml:

services:
  app:
    image: myapp:latest
    ulimits:
      nofile:
        soft: 65536
        hard: 65536

Set default limits for all containers in Docker daemon config:

// /etc/docker/daemon.json
{
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    }
  }
}
sudo systemctl restart docker

When using Docker Compose, also check that the Docker Compose networking is not breaking for services that exchange many connections — leaking sockets from a misconfigured network counts toward your nofile limit just like real files.

Still Not Working?

Check if root also needs limits raised. Root has its own limits and /etc/security/limits.conf entries for root require explicit root soft nofile entries — the * wildcard does not apply to root on some systems.

Check PAM configuration. If pam_limits.so is not loaded in the PAM session configuration, limits.conf changes have no effect. Verify /etc/pam.d/common-session (Debian/Ubuntu) or /etc/pam.d/system-auth (RHEL) contains session required pam_limits.so.

Check per-process vs system limits. A process’s limit (/proc/<PID>/limits) is set at startup and does not change when ulimit or limits.conf changes. The application must be restarted after changing limits for the new limits to take effect.

Verify the actual limit in effect for the process:

cat /proc/<PID>/limits
# Look for: Max open files  65536  65536  files

If this still shows the old limit after changes, the service was not restarted correctly, or the limits were not applied to the user/service that starts the process.

Use prlimit to change a running process’s limits without restarting. This is a last resort but useful when you cannot restart a critical service:

sudo prlimit --pid <PID> --nofile=65536:65536
cat /proc/<PID>/limits | grep "Max open files"

The new limit applies immediately to the running process. It does not persist — the next restart reverts to the configured limit.

Check for inherited limits from supervisors. If you start your app via supervisord, pm2, runit, or another process supervisor, that supervisor’s own limits are inherited by all children. Setting LimitNOFILE in a systemd unit that runs pm2 is not enough — also set pm2’s own max_open_files in its config, or use the supervisor’s documented mechanism.

Check for FD-hungry libraries. Some libraries open many FDs per connection: libuv (Node.js) keeps an FD per file watcher, gRPC opens one per stream, and some database drivers keep a pool of long-lived sockets. Sum up the per-connection FD count against your concurrent connection peak — if the math exceeds your limit, raise it before chasing leaks.

Check Kubernetes pod-level limits. If your app runs in K8s and cat /proc/<PID>/limits shows the wrong value, the limit is being inherited from the container runtime, not the pod spec. For nginx ingress hitting this, see also Kubernetes ingress not working for related ingress-side debugging. For container-level OOM that often co-occurs under fd pressure, see Docker exited 137 OOMKilled.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles