"Too Many Open Files" on Your VPS: Fixing ulimit Limits

· 6 min read · 20 views · Getwebup

Your VPS was running fine for months. Then, out of nowhere, Nginx starts throwing 500s, MySQL refuses new connections, or PHP-FPM workers just stop responding — and the only clue in the logs is a cryptic line: Too many open files. Nothing crashed from lack of RAM or disk. You just hit a ceiling nobody told you existed.

What "too many open files" actually means

Every process on Linux gets a limit on how many file descriptors it can hold open at once. A file descriptor isn't just a file on disk — it's also every open network socket, every log file handle, every pipe between processes. A busy Nginx server handling hundreds of concurrent connections, or a MySQL instance with dozens of open tables and client sockets, can burn through thousands of descriptors without you noticing.

The default per-process limit on most stock Linux images is 1024. That number was fine for a lightly loaded box in 2010. It is nowhere near enough for a modern WordPress + Redis + PHP-FPM stack under real traffic.

When a process hits its ceiling, it doesn't get more graceful about it — new connections just get refused, new files can't be created, and error logs fill up with variations of EMFILE or ENFILE.

Symptoms that point to a file descriptor limit

  • Nginx error log shows socket() failed (24: Too many open files)
  • MySQL error log shows Can't open file: (errno: 24 "Too many open files") or the server silently refuses connections despite max_connections looking fine
  • PHP-FPM workers log unable to open primary script even though the file clearly exists
  • Redis logs Error accepting a client connection: accept: Too many open files
  • The problem gets worse under load and clears up (temporarily) after a restart of the affected service

That last point is what throws people off. A service restart resets its open-file count to near zero, so the symptom disappears for a while and comes back once traffic builds back up. It looks like a fluke instead of a hard limit being hit.

Check your current limits

Start by checking the system-wide and per-process ceilings. SSH into the VPS and run:

ulimit -n

This shows the soft limit for the current shell session — usually 1024. That number does not tell you what a running service is actually using. For that, find the process ID and read its limits directly from /proc:

pgrep -f nginx
cat /proc/<PID>/limits | grep "Max open files"

To see how many descriptors a process is actually holding open right now:

ls -1 /proc/<PID>/fd | wc -l

If that number is sitting close to the "Max open files" value, you've found your bottleneck. Also check the kernel-wide ceiling, which caps every process combined:

cat /proc/sys/fs/file-max
cat /proc/sys/fs/file-nr

The three numbers in file-nr are: currently allocated descriptors, currently unused, and the max. If the first number is creeping up toward the third across your whole server, several services are competing for the same shrinking pool.

The fix: raise limits at every layer

This is the part people get wrong — there isn't one setting to change. Linux enforces limits at three separate layers, and a low value at any one of them overrides a higher value set elsewhere.

1. Kernel-wide limit

Edit /etc/sysctl.conf (or drop a file in /etc/sysctl.d/) and set a generous system-wide cap:

fs.file-max = 2097152

Apply it without a reboot:

sysctl -p

2. Per-user limits via limits.conf

Edit /etc/security/limits.conf and add lines for the user that runs your services (often www-data, nginx, mysql, or a custom deploy user):

www-data soft nofile 65535
www-data hard nofile 65535
mysql    soft nofile 65535
mysql    hard nofile 65535
* soft nofile 65535
* hard nofile 65535

If your services are managed by systemd (true for most modern VPS images), limits.conf is often ignored entirely for daemon processes, because systemd applies its own limits before PAM ever gets involved. This is the step people miss most often.

3. systemd service overrides

For each service, create an override rather than editing the shipped unit file:

systemctl edit nginx

Add:

[Service]
LimitNOFILE=65535

Repeat for mysql, php8.3-fpm (or your version), and redis-server as needed. Then reload systemd and restart the service:

systemctl daemon-reload
systemctl restart nginx

Confirm it took effect:

systemctl show nginx | grep LimitNOFILE
cat /proc/$(pgrep -f "nginx: master")/limits | grep "Max open files"

Service-specific settings worth knowing

ServiceWhat to checkWhere
Nginxworker_rlimit_nofile should match or exceed the systemd limit/etc/nginx/nginx.conf
MySQL/MariaDBopen_files_limit variable/etc/mysql/mariadb.conf.d/50-server.cnf
PHP-FPMpool rlimit_files/etc/php/8.3/fpm/pool.d/www.conf
Redismaxclients plus the OS limit above it/etc/redis/redis.conf

For Nginx specifically, set worker_rlimit_nofile explicitly in the main context so it doesn't just inherit whatever the shell happened to have:

worker_rlimit_nofile 65535;

events {
    worker_connections 8192;
}

For MySQL, check what it actually negotiated at startup — it will silently cap itself lower than your requested value if the OS won't allow it:

SHOW VARIABLES LIKE 'open_files_limit';

Prevention: catch it before it's a fire

  • Baseline your descriptor usage once things are stable, so you know what "normal" looks like per service.
  • Alert on it. If you're already running Netdata or a similar monitor, add a check on open file descriptors per process, not just CPU/RAM/disk.
  • Set limits deliberately, not by accident. Don't just blanket-set everything to unlimited — a runaway process leaking file handles should still hit a wall and get logged, rather than quietly consuming the entire kernel table and taking every other service down with it.
  • Re-check after OS or package upgrades. A distro upgrade or a fresh systemd unit file from a package update can silently reset your overrides back to defaults.

When this shows up after scaling traffic

If you've recently added a CDN, launched a marketing campaign, or moved several sites onto one VPS, this is exactly the kind of limit that only bites once real concurrency shows up in testing or staging never triggers it. Budget descriptor headroom the same way you budget RAM and CPU when you scale — it's cheap to raise and expensive to debug blind at 2 a.m.

Questions people actually ask