Your PHP site runs fine, then traffic spikes and nginx starts throwing 502 Bad Gateway. Logs blame the upstream. The cause is almost always one number in your PHP-FPM pool: pm.max_children.
What actually happens
Nginx hands PHP requests to PHP-FPM. FPM has a fixed ceiling of worker processes — pm.max_children. When every worker is busy and the listen backlog fills, FPM can't accept the connection, nginx's fastcgi_pass fails, and the user gets a 502. Under pm = dynamic, you'll also see this in the FPM status page:
max children reached: 37
listen queue: 120
That counter is the smoking gun: you've hit the ceiling 37 times.
How to size max_children (stop guessing)
The honest formula:
max_children = (RAM budget for FPM) / (average worker RSS)
- Measure a real worker's resident memory (RSS) under your actual app, not
memory_limit(that's a cap, not usage). - Pick a RAM budget — total RAM minus what the DB, cache, and OS need. Leave headroom.
- Divide. That's your ceiling. Set
pm.max_childrento it, not to a number you copied from a blog.
Too high and a traffic spike spawns more workers than RAM allows → the OOM killer starts reaping processes (worse than a 502). Too low and you 502 under load you could have served.
The shared-pool 502 storm
The nastiest version: multiple sites share one FPM master and one max_children. One busy site starves the others, and they all 502 — even the idle ones. Fix: give each site its own pool with its own ceiling, sized from its own RSS.
Also worth setting
-
pm.max_requests(recycle workers to bound memory leaks). - Watch
slow requestsin the status page and the slowlog — a few slow endpoints hold workers hostage and fake a capacity problem.
I turned this into a read-only CLI, fpm-doctor — it reads your pool configs + live status, measures real worker RSS, and recommends max_children from your actual memory budget, flags the shared-pool pattern, and ranks your slowlog. Composer + .phar, never writes your config.
Top comments (0)