Fix "Too many open files" (EMFILE) Error on High-Traffic Servers

intermediate🌐 Networking2026-04-20| Linux (Ubuntu 20.04/22.04, CentOS 7/8, Debian), nginx, Node.js, Apache, any high-connection TCP server

Error Message

Error: accept(2) failed: Too many open files (EMFILE)
#linux#networking#ulimit#socket#server

The Error

Error: accept(2) failed: Too many open files (EMFILE)

Your server hit its file descriptor ceiling and can no longer accept new connections. On Linux, every open socket counts as an FD β€” TCP connections, UDP sockets, unix sockets, and regular files all draw from the same pool. When that pool runs dry, accept(2) returns EMFILE and clients get refused.

Usually surfaces under load: stress tests, sudden traffic spikes, or a slow connection leak that's been building for hours.

Root Cause

Linux enforces two limits on open file descriptors:

  • Per-process limit (ulimit -n) β€” how many FDs a single process can hold. Defaults to 1024 on CentOS 7 and older distros, 65536 on Ubuntu 22.04+ and newer RHEL systems.
  • System-wide limit (/proc/sys/fs/file-max) β€” total FDs across all processes on the host.

The per-process limit is almost always the culprit. A busy nginx worker handling 600 concurrent connections will blow past 1024 FDs in seconds β€” and you won't notice until clients start getting refused.

Diagnose First

Before changing anything, check what limit the affected process actually sees at runtime:

# Find the PID
pgrep nginx  # or node, apache2, etc.

# Check its current FD limit
cat /proc/<PID>/limits | grep 'open files'

# See how many FDs it's currently using
ls /proc/<PID>/fd | wc -l

# System-wide current usage vs limit
cat /proc/sys/fs/file-nr
# Output: [open FDs] [0] [max FDs]

Current usage near the limit shown in /proc/<PID>/limits? That's your problem. Also check for connection leaks:

# Count sockets by state for your process
ss -s

# See all connections for a specific port
ss -tnp | grep ':8080'

# How many TIME_WAIT are piling up?
ss -tan | grep TIME-WAIT | wc -l

Thousands of TIME_WAIT or CLOSE_WAIT connections explain why FDs are being held longer than expected. Fix the leak first β€” raising the limit just delays the problem.

Fix 1: Raise the Per-Process ulimit (Immediate)

For a quick fix on a running system, set the limit for the current shell and restart the service:

ulimit -n 65536

This only applies to the current shell session. To persist across reboots, edit /etc/security/limits.conf:

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

# Or target a specific user (e.g., www-data for nginx)
www-data    soft    nofile    65536
www-data    hard    nofile    65536

Log out and back in (or restart the service) for this to take effect. Verify it worked:

su - www-data -s /bin/bash -c 'ulimit -n'

Fix 2: Set the Limit in the systemd Unit File

On most modern Linux servers, limits.conf won't actually help you. Systemd services often bypass PAM entirely, so per-user settings in that file never get applied to the service process. Set the limit directly in the unit instead:

# Create an override file
systemctl edit nginx

# Add this in the editor:
[Service]
LimitNOFILE=65536

Or drop a file manually:

# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=65536

Then reload and restart:

systemctl daemon-reload
systemctl restart nginx

# Confirm the new limit is active
cat /proc/$(pgrep nginx | head -1)/limits | grep 'open files'

Fix 3: Raise the System-Wide Limit

Running many services, or the per-process limit is already high but you're still hitting errors? The system-wide cap might be the bottleneck:

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

# Raise it temporarily
sysctl -w fs.file-max=2097152

# Make it permanent
echo 'fs.file-max = 2097152' >> /etc/sysctl.conf
sysctl -p

Fix 4: nginx-Specific Config

nginx has its own worker_rlimit_nofile directive that overrides the OS limit for worker processes:

# /etc/nginx/nginx.conf
worker_processes auto;
worker_rlimit_nofile 65536;

events {
    worker_connections 4096;  # per worker process
    use epoll;
    multi_accept on;
}

The math: worker_connections Γ— worker_processes Γ— 2 ≀ worker_rlimit_nofile. Each connection uses at least 2 FDs β€” one for the client socket, one for the upstream or file being served.

Fix 5: Node.js Servers

Node.js inherits its FD limit from whatever process launched it β€” there's no Node-specific config. A server handling 2,000 concurrent WebSocket connections needs at least 2,000 FDs just for those sockets, before counting DB connections, log file handles, or anything else. Make sure the shell or systemd unit launching node has the right limit set. You can log it at startup to confirm:

const { execSync } = require('child_process');

// Log current FD limit at startup
try {
  const limit = execSync('ulimit -n').toString().trim();
  console.log(`FD limit: ${limit}`);
} catch (e) {
  console.error('Could not read FD limit');
}

Using PM2? The app config alone isn't enough β€” set LimitNOFILE in the PM2 systemd service too:

# ecosystem.config.js
module.exports = {
  apps: [{
    name: 'myapp',
    script: 'server.js',
    node_args: '--max-old-space-size=2048',
    env: {
      NODE_ENV: 'production'
    }
  }]
};

# And in /etc/systemd/system/pm2-root.service.d/override.conf:
# [Service]
# LimitNOFILE=65536

Verify the Fix

After restarting, check the limit is actually in effect β€” don't assume:

# Confirm the new limit is active
cat /proc/$(pgrep -f 'nginx: master' | head -1)/limits | grep 'open files'

# Watch FD usage in real time under load
watch -n1 'ls /proc/$(pgrep nginx | head -1)/fd | wc -l'

# Run a quick load test to confirm it doesn't hit EMFILE anymore
# (install with: sudo apt install apache2-utils)
ab -n 10000 -c 500 http://localhost/

# Check error log β€” should be clean
tail -f /var/log/nginx/error.log

Prevention

A few things that have saved me from hitting this again:

  • Set limits in systemd units, not just limits.conf. On systemd-managed systems, the unit file takes precedence. Always set LimitNOFILE there.
  • Monitor FD usage. Alert when any process exceeds 80% of its FD limit. Prometheus node_exporter exposes node_filefd_allocated and node_filefd_maximum β€” a simple alert rule like (node_filefd_allocated / node_filefd_maximum) > 0.8 gives you early warning before things break.
  • Enable SO_REUSEPORT on multi-worker servers to distribute accept load across workers and reduce the chance of one worker saturating its FD budget.
  • Tune TIME_WAIT recycling if you see thousands of lingering connections eating FDs:

/etc/sysctl.conf

net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 15

  
  - **Audit connection handling in your app.** HTTP clients, DB connection pools, and file handles need explicit close calls. A connection leak will exhaust FDs even with a generous limit β€” 65536 FDs disappears fast when nothing is cleaning up.

When debugging which IP ranges are hammering your server, the [Subnet Calculator on ToolCraft](https://toolcraft.app/en/tools/developer/ip-subnet-calculator) is handy for quickly working out CIDR ranges β€” runs entirely in the browser, nothing uploaded.

Related Error Notes