Performance7 min readPublished: 6 Dec 2024Updated: 29 Jul 2026

How to Tune PHP-FPM for Performance

Size PHP-FPM pools from real memory figures, choose between dynamic and ondemand, and fix the 502 errors that come from max_children being set too low.

G7Cloud Engineering
Platform team
Share:
Abstract illustration representing how to tune php-fpm for performance.
  • 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 ps on 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 reached in 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.

Measure, then divide
# 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 -15
Adjust php-fpm8.3 to your version. ps -ylC php-fpm --sort:rss if the binary has no version suffix.
StepExample figure
Total RAM4096 MB
Reserve for MariaDB1024 MB
Reserve for Nginx, the OS and headroom768 MB
Available for PHP2304 MB
Measured average process size80 MB
max_children = 2304 / 8028

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.

ModeBehaviourUse when
dynamicKeeps spare workers ready, scales up under loadA normal site: the default choice
ondemandStarts workers per request, kills them when idleMany low-traffic sites on one server
staticAll workers running all the timeOne busy site with memory to spare

If you are unsure, use dynamic. It is a safe default at almost any traffic level.

/etc/php/8.3/fpm/pool.d/example.conf
[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-status
One pool per site with its own user is the arrangement to aim for: a runaway script then cannot starve the other sites on the box.
Pro Tip

pm.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 message to search for
# 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
One occurrence during an unusual spike is fine. The same warning several times an hour means requests are queueing routinely.
SymptomUsually means
502 Bad Gateway under loadNo worker free: max_children too low, or workers wedged
504 Gateway TimeoutA request ran longer than the proxy timeout
Site fine at 2am, slow at 2pmCapacity, not code: workers are exhausted at peak
Out of memory, processes killedmax_children too high for the RAM available
Slow but no errorsNot 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.

Nginx location block
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;
}
allow 127.0.0.1 then deny all. The status page reveals your capacity and should never be public.
Reading it
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.
FieldHealthyAction if not
listen queue0Requests are waiting: raise max_children if RAM allows
max children reached0Anything above 0 means the ceiling was hit
max active processesBelow max_childrenIf it equals max_children you have no headroom left
slow requestsNear 0Read 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.

/etc/php/8.3/fpm/conf.d/10-opcache.ini
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 reset
Do not set opcache.save_comments=0. It saves a little memory and breaks any library that reads annotations.

Leave 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.

Put this into practice on G7Cloud

Every site runs in its own dedicated container behind ScaleShield, with daily backups that are restore-tested every night. Start on the free plan, no card needed.

About G7Cloud Engineering

Platform team

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 →