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 despitemax_connectionslooking fine - PHP-FPM workers log
unable to open primary scripteven 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 -nThis 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 -lIf 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-nrThe 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 = 2097152Apply it without a reboot:
sysctl -p2. 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 65535If 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 nginxAdd:
[Service]
LimitNOFILE=65535Repeat 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 nginxConfirm it took effect:
systemctl show nginx | grep LimitNOFILE
cat /proc/$(pgrep -f "nginx: master")/limits | grep "Max open files"Service-specific settings worth knowing
| Service | What to check | Where |
|---|---|---|
| Nginx | worker_rlimit_nofile should match or exceed the systemd limit | /etc/nginx/nginx.conf |
| MySQL/MariaDB | open_files_limit variable | /etc/mysql/mariadb.conf.d/50-server.cnf |
| PHP-FPM | pool rlimit_files | /etc/php/8.3/fpm/pool.d/www.conf |
| Redis | maxclients 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.