PHP-FPM runs a pool of PHP worker processes. Nginx or Apache passes PHP requests to them. Too few workers and requests queue: the site gets slow and you see 502/504 errors. Too many and the server runs out of memory and starts swapping or killing processes. Tuning PHP-FPM is mostly about picking the right number of workers for your RAM.
Where the configuration lives
On Debian/Ubuntu (replace 8.3 with your version):
/etc/php/8.3/fpm/php-fpm.conf— global settings/etc/php/8.3/fpm/pool.d/www.conf— the default pool
On RHEL-family systems: /etc/php-fpm.d/www.conf.
After any change:
sudo php-fpm8.3 -t && sudo systemctl reload php8.3-fpm
Process manager modes
pm = dynamic ; or static, ondemand
| Mode | Behaviour | Good for |
|---|---|---|
static |
Always runs pm.max_children workers |
Busy, dedicated app servers with steady traffic |
dynamic |
Keeps between min_spare and max_spare idle workers, up to max_children |
General purpose (the default) |
ondemand |
Starts workers when requests arrive and stops them after pm.process_idle_timeout |
Many low-traffic sites on one server, or small VPS plans |
Calculating pm.max_children
This is the most important setting. It caps memory use:
pm.max_children = (RAM available for PHP) / (average memory per PHP worker)
1. Measure worker memory under real traffic:
ps --no-headers -o rss -C php-fpm8.3 | awk '{sum+=$1; n++} END {print sum/n/1024 " MB average over " n " processes"}'
WordPress workers often use 40–80 MB. Heavy plugins, WooCommerce or Magento can push that to 100 MB or more.
2. Work out the RAM available for PHP. Take total RAM and subtract the OS, the database (MySQL’s buffer pool), Redis and other services. Check with the free command.
Example: a 4 GB VPS running MySQL with a 1 GB buffer pool. Leave about 1.2 GB for the OS, Nginx, MySQL overhead and a margin. That leaves about 1.8 GB for PHP. At 60 MB per worker, 1800 / 60 = 30.
For dynamic, a common starting point is:
pm = dynamic
pm.max_children = 30
pm.start_servers = 6
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 500
pm.max_requests restarts each worker after that many requests. This contains slow memory leaks in plugins and extensions.
For ondemand:
pm = ondemand
pm.max_children = 20
pm.process_idle_timeout = 10s
pm.max_requests = 500
Does more workers mean more speed?
Only up to a point. Each worker handles one request at a time. If requests mostly wait on the database or external APIs, more workers help, as long as you have the RAM. If requests are CPU-bound, workers beyond about 2–4× the CPU core count just compete for CPU. Then the fix is caching (OPcache, page caching, Redis object cache), not more workers.
One pool per site
On a server with several sites, give each site its own pool:
; /etc/php/8.3/fpm/pool.d/example.conf
[example]
user = example
group = example
listen = /run/php/php8.3-fpm-example.sock
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 10
php_admin_value[open_basedir] = /var/www/example.com:/tmp
php_admin_value[error_log] = /var/log/php/example-error.log
This has three benefits:
- Isolation: a compromised site can’t read the others’ files.
- Fairness: one busy site can’t use up every worker.
- Per-site settings: memory limits, upload size, PHP version.
Point the site’s Nginx fastcgi_pass (or Apache SetHandler) at its own socket. See Nginx with PHP-FPM.
Find slow scripts
request_slowlog_timeout = 5s
slowlog = /var/log/php8.3-fpm-slow.log
request_terminate_timeout = 120s
The slow log records a stack trace for any request that runs longer than 5 seconds. It shows exactly which plugin or function is slow. request_terminate_timeout kills runaway requests.
The status page
pm.status_path = /fpm-status
Expose it to localhost only in Nginx:
location = /fpm-status {
allow 127.0.0.1;
deny all;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
Then run curl http://127.0.0.1/fpm-status. Watch these values:
- listen queue — requests waiting for a worker. It should be 0.
- max children reached — how often the pool hit the limit. If it keeps climbing, raise
max_children(if you have the RAM) or reduce the work each request does. - active / idle processes.
Common errors
“server reached pm.max_children setting” in the FPM log. The pool is full and requests are queueing. Check RAM and raise the limit, add caching, or find slow requests in the slow log.
502 Bad Gateway. FPM is down, the socket path is wrong, or workers crash. See Nginx 502 Bad Gateway.
504 Gateway Timeout. Requests take longer than the proxy timeout. Find the slow code. Raise fastcgi_read_timeout only for endpoints that really are long-running.
The OOM killer stops PHP or MySQL. max_children is too high for the RAM. Lower it, and consider adding swap as a safety net. See swap file vs partition.
OPcache: free performance
Make sure OPcache is enabled (php -i | grep opcache.enable). It caches compiled scripts. Reasonable production values:
opcache.enable=1
opcache.memory_consumption=192
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=60
Related: php.ini settings and Nginx FastCGI cache.