PHP-FPM keeps a pool of worker processes, and each one handles one request at a time. Too few workers and visitors wait in a queue or get 502 and 504 errors. Too many and the server runs out of RAM, starts swapping or has MySQL killed by the out-of-memory killer. This guide shows how to measure what one worker really uses on your server and turn that into safe pool settings.
Before you start
- A Linux VPS running PHP-FPM behind Nginx, Apache or Caddy, with a sudo user.
- Your site running with normal traffic for a while, so workers have reached their typical size.
- Your pool file. On Ubuntu and Debian it is
/etc/php/8.3/fpm/pool.d/www.conf(adjust the version). On AlmaLinux and Rocky Linux it is/etc/php-fpm.d/www.conf. If you use one pool per site, each has its own file.
The process managers
The pm setting decides how PHP-FPM manages workers:
dynamickeeps a few idle workers ready and adds more under load, up topm.max_children. It is the default and a good choice for most busy sites.ondemandstarts with no workers, forks them when requests arrive and kills them afterpm.process_idle_timeout. It saves memory on servers with many low-traffic sites, at the cost of a small delay on the first request after a quiet period.staticalways runs exactlypm.max_childrenworkers. It gives the most predictable performance on a server dedicated to one busy app, but holds all that memory at all times.
Whatever mode you pick, pm.max_children is the hard ceiling, and it is the number that protects your RAM.
Measure memory per worker
Use the average resident memory (RSS) of the running workers. The process name includes the version on Debian and Ubuntu:
ps --no-headers -o rss -C php-fpm8.3 | awk '{sum+=$1; n++} END {printf "workers: %d, avg: %.0f MB\n", n, sum/n/1024}'On AlmaLinux and Rocky Linux, the process is called php-fpm:
ps --no-headers -o rss -C php-fpm | awk '{sum+=$1; n++} END {printf "workers: %d, avg: %.0f MB\n", n, sum/n/1024}'To see the largest workers, which matter more than the average on sites with heavy pages:
ps -o pid,rss,cmd -C php-fpm8.3 --sort=-rss | headRSS counts shared memory, such as the OPcache, in every process, so this slightly overestimates. That bias is safe. Take the measurement at a busy time, and round up.
Calculate pm.max_children
First decide how much RAM PHP may use. Start from the total and subtract everything else:
free -mSubtract the memory used by the database, the web server, Redis and anything else, plus a safety margin for the OS and file cache. What is left is the PHP budget.
Then:
pm.max_children = PHP budget (MB) / largest typical worker (MB)A worked example with made-up numbers for illustration:
- The server has 4096 MB. MariaDB uses about 800 MB, Nginx and Redis about 200 MB, and you keep 600 MB spare.
- PHP budget: 4096 - 800 - 200 - 600 = 2496 MB.
- Workers measure 60 to 90 MB. Use 90 MB.
- 2496 / 90 = 27.7, so set
pm.max_children = 27or round down to 25.
Measure your own server. Worker size depends heavily on your code, plugins and memory_limit.
Note: memory_limit in php.ini is the most a single request may use. A few heavy requests can push workers far above the average, so leave a margin rather than using every megabyte.
Set the pool values
Edit the pool file and set values that fit your calculation. For dynamic:
pm = dynamic
pm.max_children = 25
pm.start_servers = 6
pm.min_spare_servers = 4
pm.max_spare_servers = 8
pm.max_requests = 500Common rules of thumb:
pm.start_serversmust be betweenmin_spare_serversandmax_spare_servers. About a quarter ofmax_childrenis a reasonable start.pm.max_requestsrestarts each worker after that many requests, which releases memory leaked by extensions or long-running code. A value of a few hundred to a thousand is typical.
For ondemand:
pm = ondemand
pm.max_children = 25
pm.process_idle_timeout = 10s
pm.max_requests = 500Test the config, then reload so running requests finish cleanly:
sudo php-fpm8.3 -t
sudo systemctl reload php8.3-fpmIf several pools run on the same server, their pm.max_children values add up. The total across all pools must fit in the PHP budget, not each pool separately.
The max_children warning
When every worker is busy and a new request arrives, PHP-FPM logs a line like this:
WARNING: [pool www] server reached pm.max_children setting (25), consider raising itFind how often it happens:
sudo grep -c "max_children" /var/log/php8.3-fpm.log
sudo grep "max_children" /var/log/php8.3-fpm.log | tail -5Do not raise the limit blindly. First ask why workers are busy:
- Slow requests: a slow database query or external API call holds a worker for seconds. More workers only add more waiting processes. Turn on the slow log to find the culprit:
request_slowlog_timeout = 5s
slowlog = /var/log/php8.3-fpm-slow.log- Real traffic growth: if requests are fast and you still hit the limit, raise
pm.max_childrenonly if the RAM calculation allows it. Otherwise add page caching or move to a larger server. - Bots: a crawler hitting search or cart pages can fill the pool. Check the web server access log for the top client IPs.
The PHP-FPM status page shows active, idle and queued workers live, which makes this much easier to judge.
Check it works
Watch memory and workers while the site is busy:
watch -n 2 "free -m; echo; ps --no-headers -C php-fpm8.3 | wc -l"Available memory should stay well above zero, swap use should stay flat, and the worker count should never go past pm.max_children plus the master process. Recheck the log for the warning after a day.
Troubleshooting
- Server swaps or the OOM killer stops MySQL:
pm.max_childrenis too high for the RAM. Lower it, then checkdmesg -T | grep -i "out of memory"for past kills. See Fix MySQL crashes from the OOM killer. - 502 or 504 errors under load with free RAM left: the pool is too small or requests are slow. Use the slow log before raising the limit.
- PHP-FPM will not reload after edits:
pm.start_serversis outside the spare range, or a value has a typo.php-fpm8.3 -tnames the problem. - Memory climbs all day then drops on restart: set or lower
pm.max_requestsso workers recycle.
Next steps
- Isolate sites and give each its own budget: Run each site in its own PHP-FPM pool and user.
