Your WordPress or Laravel site runs fine at normal traffic, then a campaign email goes out, or a post gets shared somewhere, and suddenly half your visitors see "502 Bad Gateway" while the other half get a slow-but-working page. You check CPU and RAM and neither is maxed out. The actual bottleneck is usually simpler: PHP-FPM ran out of worker processes and started queuing or rejecting requests. Here's how to size pm.max_children correctly instead of guessing a bigger number and hoping.

Symptom: What Pool Exhaustion Looks Like

A handful of signs point at PHP-FPM's process pool rather than the server as a whole:

  • 502 errors that come and go with traffic, not a fixed config problem that's always broken
  • Nginx error log full of upstream sent too big header or, more tellingly, server reached pm.max_children setting
  • Server load and free memory look okay in top, but the site still times out for some visitors
  • Restarting PHP-FPM "fixes" it for a few minutes, then it comes back once traffic returns

That last one is the giveaway. Restarting clears the backlog, it doesn't fix the sizing.

Cause: Every Worker Is a Full PHP Process, Not a Thread

PHP-FPM manages a pool of child processes. Each one handles exactly one PHP request at a time — there's no internal concurrency inside a single worker. If your pool has 10 workers and 15 requests arrive at once, 10 get processed and 5 sit in a queue (or get dropped once the queue itself fills up). Nginx, having waited long enough, gives up and returns a 502 to the visitor who was queued.

Most installs ship with a default pm.max_children that was picked for a generic server, not yours — often 5, 10, or 50 depending on the distro's package. It has no relationship to your actual RAM or your actual PHP process size, which is exactly why it breaks under load that a "healthy-looking" server should be able to handle.

Fix: Calculate the Real Number, Then Set It

1. Find out how much memory one PHP-FPM worker actually uses

Don't guess this — measure it while the site is under normal load:

ps --no-headers -o "rss,cmd" -C php-fpm8.3 | awk '{ sum+=$1; count++ } END { print sum/count/1024 " MB average" }'

Adjust the process name (php-fpm8.3, php-fpm7.4, etc.) to match your installed version — check with ps aux | grep fpm first. A typical WordPress worker with a few plugins sits around 40–90MB; a heavier Laravel or WooCommerce site can run 100–180MB per worker.

2. Work out how much RAM you can actually give PHP-FPM

Leave headroom for MySQL/MariaDB, Nginx, Redis, cron jobs, and the OS itself. A rough split on a shared web+database VPS:

Total VPS RAMSafe budget for PHP-FPM pool
2GB~700MB–900MB
4GB~1.8GB–2.2GB
8GB~4GB–5GB

If MySQL lives on a separate server, you can push these numbers higher.

3. Do the math

The formula is straightforward:

pm.max_children = PHP-FPM RAM budget / average worker size

Example: 2GB budget, 80MB average worker → pm.max_children = 25. Don't round up "just in case" — if real usage spikes above your measured average (a plugin doing something heavy), overcommitting pm.max_children is how a traffic spike turns into an out-of-memory crash instead of a clean 502.

4. Pick the right process manager mode

This matters as much as the number itself:

ModeBehaviorBest for
staticAlways runs exactly pm.max_children workers, idle or notBusy sites with steady traffic — no spin-up delay, predictable RAM use
dynamicScales between pm.min_spare_servers and pm.max_children based on demandMost sites — good balance, needs more tuning knobs
ondemandStarts workers only when a request arrives, kills idle ones after pm.process_idle_timeoutLow-traffic sites or memory-constrained VPS where idle RAM matters more than response latency

For most single-site VPS setups, dynamic is the sane default. Edit your pool file, usually at /etc/php/8.3/fpm/pool.d/www.conf:

pm = dynamic
pm.max_children = 25
pm.start_servers = 6
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 500

pm.start_servers should sit between min_spare_servers and max_spare_servers — PHP-FPM refuses to start otherwise. pm.max_requests recycles a worker after that many requests, which is cheap insurance against a slow memory leak in a plugin.

5. Apply it and confirm

php-fpm8.3 -t
systemctl reload php8.3-fpm

Then watch the pool status live for a day or two under real traffic:

curl http://127.0.0.1/status?full

(This needs pm.status_path = /status set in the pool config and a matching Nginx location block restricted to localhost.) Watch the max children reached counter — if it keeps climbing, your budget or worker-size math needs revisiting, not just a bigger flat number.

Prevention: Don't Let This Sneak Back In

  • Re-measure average worker size after major plugin or framework updates — a new page builder or ORM can quietly double memory per request
  • If you're on shared cPanel hosting instead of a VPS, you don't control pm.max_children directly — this is a VPS/dedicated-server tuning topic. On cPanel with FPM enabled per-domain, MultiPHP Manager exposes a simplified version of the same settings
  • Set up basic alerting (even a simple cron script checking the status page) so you find out about pool exhaustion before your visitors do
  • If you're consistently maxing out a well-sized pool, that's a real capacity signal — it's time to look at caching (OPcache, a page cache) or a bigger VPS plan, not just cranking the number further

Frequently Asked Questions

What happens if I set pm.max_children too high?

If every worker actually spikes to its peak memory size at once — a burst of traffic hitting a memory-heavy page — the server can run out of RAM entirely, triggering the OOM killer. That's a much messier failure than a clean 502, so it's safer to size conservatively and raise the VPS plan than to overcommit.

How do I know which PHP-FPM version and pool file I'm actually editing?

Run php -v for the CLI version, but check ps aux | grep fpm for what's actually running the web requests — they can differ if you've got multiple PHP versions installed. The pool file path follows /etc/php/<version>/fpm/pool.d/www.conf on Debian/Ubuntu, or /etc/php-fpm.d/www.conf on AlmaLinux/Rocky.

Why do I see "server reached pm.max_children" without any 502 errors?

Nginx will queue requests briefly rather than fail instantly, so a short spike above your pool size can resolve as a slow page load instead of an outright error. It's still worth treating as a warning — the next spike, or a slightly bigger one, is what turns into visible 502s.

Does raising pm.max_requests fix memory leaks?

No — it limits the damage. Recycling a worker after N requests prevents a slow leak from eventually consuming all its memory, but if a specific plugin or code path is leaking badly, you should still find and fix it rather than relying on recycling to paper over it.

Is this relevant if I'm using LiteSpeed instead of PHP-FPM?

LiteSpeed has its own worker model (LSAPI) with a similar but separately configured concept — PHP_LSAPI_MAX_REQUESTS and the LSWS process settings play the same role. The underlying idea — measure worker memory, budget RAM, size the pool — carries over even though the config keys are different.