Websites & apps

Tune PHP-FPM pools (pm.max_children) for your RAM

Size PHP-FPM to your server's memory. Choose between dynamic, ondemand and static, calculate pm.max_children from real per-process memory, set pm.max_requests, and act on the max_children warning.

AdvancedLinux server6 min read

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:

  • dynamic keeps a few idle workers ready and adds more under load, up to pm.max_children. It is the default and a good choice for most busy sites.
  • ondemand starts with no workers, forks them when requests arrive and kills them after pm.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.
  • static always runs exactly pm.max_children workers. 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:

Bash
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:

Bash
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:

Bash
ps -o pid,rss,cmd -C php-fpm8.3 --sort=-rss | head

RSS 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:

Bash
free -m

Subtract 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 = 27 or 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:

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

Common rules of thumb:

  • pm.start_servers must be between min_spare_servers and max_spare_servers. About a quarter of max_children is a reasonable start.
  • pm.max_requests restarts 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:

INI
pm = ondemand
pm.max_children = 25
pm.process_idle_timeout = 10s
pm.max_requests = 500

Test the config, then reload so running requests finish cleanly:

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

If 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 it

Find how often it happens:

Bash
sudo grep -c "max_children" /var/log/php8.3-fpm.log
sudo grep "max_children" /var/log/php8.3-fpm.log | tail -5

Do 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:
INI
  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_children only 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:

Bash
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_children is too high for the RAM. Lower it, then check dmesg -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_servers is outside the spare range, or a value has a typo. php-fpm8.3 -t names the problem.
  • Memory climbs all day then drops on restart: set or lower pm.max_requests so workers recycle.

Next steps

  • Isolate sites and give each its own budget: Run each site in its own PHP-FPM pool and user.
Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.