VPS "fork: Resource Temporarily Unavailable": Fix nproc Limits
You SSH into your VPS to restart a service and get slapped with -bash: fork: retry: Resource temporarily unavailable. Or a customer's PHP script that used to work fine suddenly can't spawn a subprocess. Nothing in top looks unusual — CPU is fine, RAM has headroom, disk isn't full. You've hit a completely different ceiling: the number of processes a single user is allowed to run at once.
Symptom: "fork: retry: Resource temporarily unavailable"
This one shows up in a handful of disguises depending on what triggered it:
- Logging in over SSH gives
fork: retry: Resource temporarily unavailableor the session just hangs and drops - A cron job fails silently, or its log shows
-bash: fork: Cannot allocate memory(misleading — it's not actually a memory problem) - PHP-FPM or a Node app throws
EAGAINwhen trying to spawn a child process - WHM/cPanel shows the account hitting its "Number of Processes" limit in the Resource Usage graph, or CloudLinux logs an LVE
nprochit - Running
ps -u yourusername | wc -lreturns a number suspiciously close to a round figure like 100, 512, or 1024
The confusing part is that free -m and df -h both look healthy. That's the tell: this isn't memory or disk, it's a process-count limit.
Cause: you've hit the nproc limit, not the open-files limit
Linux enforces several independent ceilings per user, and it's easy to confuse them. If you've read our post on too many open files errors, this is the sibling problem — same family of controls, completely different resource:
| Limit | Controls | Check with | Typical error |
|---|---|---|---|
ulimit -n | Open file descriptors (sockets, log files, pipes) | ulimit -n | Too many open files, EMFILE |
ulimit -u | Number of processes/threads a user can run | ulimit -u | fork: retry: Resource temporarily unavailable |
Kernel pid_max | Total PIDs across the whole server | cat /proc/sys/kernel/pid_max | Server-wide fork failures, rare |
| CloudLinux LVE nproc | Per-cPanel-account process cap | WHM → Edit LVE / lveinfo | Account-specific hangs, other accounts unaffected |
On a plain Ubuntu/Debian/AlmaLinux VPS, the process limit comes from PAM's limits.conf mechanism, plus a per-service override if the process was started by systemd. On a cPanel VPS running CloudLinux, there's a second layer on top: each cPanel account runs inside an LVE (Lightweight Virtual Environment) with its own nproc ceiling, independent of the OS-level limit.
The usual triggers:
- A WordPress site with a runaway cron (WP-Cron firing hundreds of overlapping requests because a previous run never finished)
- A backup script or import job that spawns a subprocess per file instead of reusing one
- A PHP-FPM pool set to a huge
pm.max_childrenthat's technically legal for the OS but blows past the cPanel account's LVE nproc cap - An actual fork bomb — a compromised script or a malicious cron doing
:(){ :|:& };:-style recursive forking - Node.js or Python worker pools (Celery, PM2 cluster mode) configured with more workers than the box was sized for
Fix: find what's forking, then raise (or correct) the limit
1. Identify who's eating your process budget
Before touching any limit, find out what's actually running:
ps -eLf | awk '{print $2}' | sort | uniq -c | sort -nr | head -20
ps -u www-data --no-headers | wc -l
ps -u yourcpaneluser --no-headers | wc -l
The first command counts threads/processes grouped by parent PID so you can spot which one is spawning hundreds of children. The next two count processes owned by a specific user — on cPanel, run the second one as the account's own user via su - accountuser -c "ps -u accountuser | wc -l".
2. Check the current limit
ulimit -u
cat /etc/security/limits.conf
ls /etc/security/limits.d/
For a systemd-managed service (Nginx, PHP-FPM, MySQL), the PAM limit is often irrelevant — systemd sets its own via DefaultTasksMax and per-unit TasksMax:
systemctl show php8.2-fpm | grep -i tasksmax
systemctl show -p DefaultTasksMax
3. Raise the limit properly
For a shell user or an application account on plain Linux, add a drop-in file rather than editing limits.conf directly (easier to track and remove later):
cat > /etc/security/limits.d/90-appuser-nproc.conf <<EOF
appuser soft nproc 4096
appuser hard nproc 8192
EOF
Log the user out and back in for it to apply — ulimit -u changes don't retroactively apply to an already-open session.
For a systemd service, don't touch PAM limits at all — override the unit instead:
systemctl edit php8.2-fpm.service
[Service]
TasksMax=2000
systemctl daemon-reload
systemctl restart php8.2-fpm
4. On cPanel/CloudLinux, raise the LVE nproc cap
If ulimit -u looks generous but the account still stalls, the LVE is the actual bottleneck. In WHM:
- Go to WHM → Edit LVE and search for the account
- Increase the Number of Processes (NPROC) value — a typical shared-hosting default is 20–25, which is far too low for a WordPress site running WP-CLI, cron, and PHP-FPM workers concurrently
- Confirm the change took effect from the account's own shell:
lveinfo $(whoami)or check WHM → Resource Usage
Don't just max this out for every account, though — a genuinely runaway process on a shared box will happily consume whatever ceiling you give it and starve its neighbours. Raise it enough to cover legitimate peak usage, not to make the symptom disappear forever.
5. If it's a fork bomb or malware, kill it before raising anything
ps -u accountuser -o pid,ppid,cmd --sort=start_time | tail -50
kill -9 <offending PID>
# or, to nuke everything owned by one user in an emergency:
pkill -9 -u accountuser
Then check crontab, wp-content, and any recently modified PHP files for the thing that spawned it — raising the nproc limit on top of active malware just buys the attacker more headroom.
Prevention
- Cap PHP-FPM sanely. Set
pm.max_childrenbased on available RAM per worker, not an arbitrary big number — this keeps you from ever approaching the LVE cap under normal load. - Fix overlapping WP-Cron runs. If a site's cron takes longer than the interval between visits, disable the default WP-Cron and drive it from a real system cron job once a minute instead — it prevents the pile-up that eats process slots.
- Monitor process counts, not just CPU/RAM. Add a simple check to your monitoring (Netdata, a cron + mail script, whatever you already run) that alerts when a user's process count crosses 70% of its limit.
- Review batch scripts. Any script that does
system(),exec(), or spawns a subprocess per row/file should reuse a small worker pool instead of forking once per item. - Set per-service TasksMax deliberately on general-purpose VPS boxes so one misbehaving service can't starve the others even before the OS-wide ceiling is reached.
FAQ
Frequently asked questions
Is "too many open files" the same problem as this one?
No. Open files (<code>ulimit -n</code>) limits file descriptors — sockets, log handles, pipes. This is <code>ulimit -u</code>, which caps the number of processes/threads a user can run. They're configured in the same <code>limits.conf</code> file but are completely independent ceilings, and the fixes don't overlap.
I raised ulimit -u but the error still happens on my cPanel account. Why?
On CloudLinux servers, cPanel accounts run inside an LVE with its own nproc cap set in WHM, separate from the OS-wide PAM limit. Raise the NPROC value under WHM → Edit LVE for that account, not just the shell ulimit.
Will raising the process limit fix my server permanently?
Only if the process count was legitimately too low for real traffic. If a fork bomb, malware, or an overlapping cron loop caused the spike, raising the limit just delays the same crash at a higher number — find and fix the root cause first.
Why does ulimit -u show a different number when I check it vs. what's in limits.conf?
limits.conf changes only apply to new login sessions created after the file is edited. If you're checking from a shell that was already open, or from a service started before the change, you'll see the old value until you log out/in or restart the service.
Does this affect systemd services the same way as SSH logins?
Not directly. Systemd-managed services (Nginx, PHP-FPM, MySQL) generally ignore PAM's limits.conf and use their own TasksMax setting instead, inherited from DefaultTasksMax unless overridden per-unit. Check both when a service — not a login shell — hits the limit.