Every PHP worker holds memory whether it is busy or idle. Too few and requests queue behind each other; too many and the machine runs out of memory, starts swapping, and becomes slower than it would have been with fewer.

Measure one worker

ps -ylC php-fpm8.3 --sort:rss | awk '{sum+=$8; n++} END {print sum/n/1024 " MB average"}'

Typically 30-80MB. A heavy framework or a large plugin set pushes it higher, and it is your own number that matters, not a typical one.

The arithmetic

max_children = (RAM available to PHP) / (average worker size)\n\n# 8GB box, 2GB for the database and the system, 50MB a worker:\n# 6000 / 50 = 120 - then be conservative and use 80
Leave headroom. A worker that hits a heavy page uses several times the average, and a pool sized to the exact limit will exceed it the moment traffic is unusual - which is exactly when you need it to hold.

Static, dynamic or ondemand

  • static - all workers started at boot. Fastest and uses the memory whether busy or not. Right for a dedicated box under steady load.
  • dynamic - keeps some spare and grows to the limit. The sensible default.
  • ondemand - starts on demand and kills after a timeout. Lowest memory, small latency on the first request. Right for many small sites on one machine.
pm = dynamic\npm.max_children = 80\npm.start_servers = 12\npm.min_spare_servers = 8\npm.max_spare_servers = 24\npm.max_requests = 500

max_requests is a leak insurance

A worker is retired after that many requests and a fresh one takes over. It costs almost nothing and contains a slow leak in any library you do not control.

Check whether you are running out

grep "server reached pm.max_children" /var/log/php8.3-fpm.log

That line means requests queued. If it never appears, the pool is large enough and raising it will not make anything faster.

On EGPHP hosting the pool is sized and monitored for you. This is for a VPS you run yourself, where the default is rarely right for the box.