- max_children is the setting that matters. Everything else is secondary.
- Work it out with arithmetic, not guesswork: available RAM divided by the average memory a PHP process actually uses.
- Too low and requests queue, producing 502 and 504 errors under load. Too high and the server runs out of memory and starts killing processes.
- Measure real process size with
pson a live server rather than trusting the memory_limit setting, which is a ceiling and not a usage figure. - Enable the PHP-FPM status page. Tuning without it is guesswork, and
max_children reachedin the log is the definitive signal.
How do I calculate the right max_children?
Take the RAM available to PHP, divide it by the average size of one PHP process, and round down. That is the whole calculation. The two mistakes are guessing the process size and forgetting that the database and web server need memory too.
# Average resident memory per PHP-FPM process, in MB
ps -ylC php-fpm8.3 --sort:rss | awk 'NR>1 {sum+=$8; n++} END {printf "avg %.0f MB across %d procs\n", sum/n/1024, n}'
# Total and available memory
free -m
# What everything else is using
ps -eo rss,comm --sort=-rss | head -15ps -ylC php-fpm --sort:rss if the binary has no version suffix.| Step | Example figure |
|---|---|
| Total RAM | 4096 MB |
| Reserve for MariaDB | 1024 MB |
| Reserve for Nginx, the OS and headroom | 768 MB |
| Available for PHP | 2304 MB |
| Measured average process size | 80 MB |
| max_children = 2304 / 80 | 28 |
Round down and leave headroom. 28 is a defensible number; 50 on the same machine is not.
memory_limit is not process size
A memory_limit of 256M does not mean each worker uses 256 MB. It is the ceiling before PHP aborts a script. A typical WordPress request uses 40 to 100 MB. Sizing max_children from memory_limit gives you a number roughly three times too small, and a site that queues under load for no reason.
Which process manager should I use?
PHP-FPM offers three. dynamic suits nearly every website: it keeps a pool of ready workers and adds more under load. ondemand starts workers only when a request arrives, which is right when several small sites share a server. static keeps every worker running permanently and only makes sense on a dedicated high-traffic machine.
| Mode | Behaviour | Use when |
|---|---|---|
| dynamic | Keeps spare workers ready, scales up under load | A normal site: the default choice |
| ondemand | Starts workers per request, kills them when idle | Many low-traffic sites on one server |
| static | All workers running all the time | One busy site with memory to spare |
If you are unsure, use dynamic. It is a safe default at almost any traffic level.
[example]
user = www-data
group = www-data
listen = /run/php/php8.3-fpm-example.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 28 ; the calculated ceiling
pm.start_servers = 7 ; 25% of max_children
pm.min_spare_servers = 4 ; never fewer ready workers than this
pm.max_spare_servers = 10 ; kill idle workers above this
pm.max_requests = 500 ; recycle a worker after 500 requests
; Log any request that takes longer than 10 seconds, with a stack trace
request_slowlog_timeout = 10s
slowlog = /var/log/php-fpm/example-slow.log
; Hard stop for a runaway request
request_terminate_timeout = 60s
; Status endpoint, restricted to localhost by the web server
pm.status_path = /fpm-statuspm.max_requests = 500 recycles each worker periodically. It does not fix a memory leak, it contains one, which is worth having when a plugin you do not control leaks slowly. The cost is a fresh process every 500 requests, which is negligible.
How do I know max_children is too low?
PHP-FPM says so, in plain English, in its log. That message is the single most useful line in PHP-FPM tuning and most people never look for it.
# The definitive signal
sudo grep -i 'max_children' /var/log/php8.3-fpm.log
# Example output:
# WARNING: [pool example] server reached pm.max_children setting (20),
# consider raising it
# Follow it live during a traffic peak
sudo tail -f /var/log/php8.3-fpm.log
# How many workers are running right now
pgrep -c php-fpm| Symptom | Usually means |
|---|---|
| 502 Bad Gateway under load | No worker free: max_children too low, or workers wedged |
| 504 Gateway Timeout | A request ran longer than the proxy timeout |
| Site fine at 2am, slow at 2pm | Capacity, not code: workers are exhausted at peak |
| Out of memory, processes killed | max_children too high for the RAM available |
| Slow but no errors | Not PHP-FPM. Look at the database or an external API |
Rows one and four are opposite failures of the same setting, which is why guessing at it is risky in both directions.
Raising max_children can make things worse
If the server is already near its memory limit, more workers means the kernel starts killing processes to reclaim RAM. It usually picks the database, because that is the largest one. Confirm with dmesg -T | grep -i oom and, if you see kills, reduce max_children rather than raising it.
How do I use the PHP-FPM status page?
The status page reports live queue depth and worker usage, which turns tuning from guesswork into reading a number. Expose it on localhost only.
location ~ ^/fpm-status$ {
access_log off;
allow 127.0.0.1;
deny all;
fastcgi_pass unix:/run/php/php8.3-fpm-example.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}curl -s http://127.0.0.1/fpm-status
# pool: example
# active processes: 12
# idle processes: 8
# total processes: 20
# max active processes: 24
# max children reached: 3 <-- this should be 0
# listen queue: 0 <-- this should be 0
# max listen queue: 18 <-- requests HAVE queued
# slow requests: 4
# Watch it during a peak
watch -n 2 'curl -s http://127.0.0.1/fpm-status'max children reached above zero and max listen queue above zero are the two numbers that justify raising max_children.| Field | Healthy | Action if not |
|---|---|---|
| listen queue | 0 | Requests are waiting: raise max_children if RAM allows |
| max children reached | 0 | Anything above 0 means the ceiling was hit |
| max active processes | Below max_children | If it equals max_children you have no headroom left |
| slow requests | Near 0 | Read the slowlog: this is application time, not capacity |
Slow requests are a different problem from queued requests, and more workers will not help with them.
What else is worth tuning?
OPcache is the highest-value setting outside the pool configuration. It caches compiled PHP bytecode so the same file is not parsed on every request, and on WordPress it typically removes a third of the CPU work for the cost of a few lines of config.
opcache.enable=1
opcache.memory_consumption=256 ; MB of shared memory for bytecode
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000 ; WordPress plus plugins easily exceeds 10000
opcache.revalidate_freq=60 ; check for changed files every 60s
opcache.save_comments=1 ; some frameworks need docblocks at runtime
opcache.validate_timestamps=1 ; set to 0 only with a deploy-time cache resetLeave the JIT alone
OPcache's JIT gives almost nothing to a typical web request, which is dominated by database and I/O time rather than raw computation, and it has produced hard-to-diagnose shared memory faults on PHP 8.3 in production. We keep it off across our whole platform for that reason. Turn it on only for a specific compute-heavy workload you have measured.
- request_terminate_timeout should be a little above your Nginx
fastcgi_read_timeout, so PHP kills a runaway script rather than leaving an orphaned worker behind after the proxy has given up. - One pool per site, each with its own user and socket. A single site's runaway loop then cannot consume every worker on the server.
- Match your PHP version to your workload. Each release since 7.4 has been meaningfully faster on the same code. PHP versions for WordPress covers upgrading safely.
- Cache in front of PHP before you tune PHP. A page served from an Nginx reverse proxy cache uses no worker at all, which beats any pool setting.
If tuning pools is not how you want to spend your time, this is the layer managed hosting exists to remove. On G7Cloud each site gets its own isolated PHP pool sized to its plan, with OPcache configured and the settings maintained centrally. See how our WordPress hosting performs.
Frequently asked questions
What should pm.max_children be set to?
Available RAM divided by the average size of one PHP-FPM process. Measure that size with ps -ylC php-fpm8.3 --sort:rss rather than assuming it, and subtract what the database and web server need first. A 4 GB server running MariaDB typically lands somewhere between 20 and 30.
Why does PHP-FPM return 502 Bad Gateway under load?
Usually because every worker is busy and new requests have nowhere to go. Check for "server reached pm.max_children setting" in the PHP-FPM log. If it appears, raise max_children, but only if you have the memory: raising it beyond what RAM supports causes the kernel to start killing processes instead.
Should I use dynamic, ondemand or static process management?
dynamic for a normal website. ondemand when several low-traffic sites share one server, since idle sites then consume nothing. static only on a dedicated machine serving one busy site where you want every worker warm and have the RAM for it.
Does increasing max_children make my site faster?
Only if requests were queueing. It adds capacity, not speed: a single request takes exactly as long as before. If pages are slow with no queue and no max_children warnings, the bottleneck is the application or database and more workers will not touch it.
What is pm.max_requests for?
It recycles a worker after that many requests, which contains memory leaks in code you do not control. 500 to 1000 is a sensible range. It is a mitigation rather than a fix, and the cost of restarting a process occasionally is negligible.
Should I enable the OPcache JIT for WordPress?
No. A typical WordPress request spends its time in the database and I/O, not in computation, so the JIT gains very little. It has also caused shared memory faults on PHP 8.3 in production. Keep OPcache itself on, which is a large win, and leave the JIT off.
About G7Cloud Engineering
Articles written by the engineers who build and run G7Cloud: UK managed hosting and the AI Website Builder. We write about what we operate every day: containers, backups, databases, and the small-business websites that run on them.
More about G7Cloud →