Every uncached WordPress request runs inside a PHP-FPM worker process. What those workers are and why they get exhausted explains the concept. This article is the practical follow-up: how to configure the pool that manages those workers so it fits the server it runs on.
PHP-FPM pool configuration lives in a dedicated file, not in php.ini. On Debian and Ubuntu that file is typically /etc/php/8.x/fpm/pool.d/www.conf; on Red Hat and derivatives it is in /etc/php-fpm.d/www.conf. The PHP manual documents every directive. The ones that matter most for WordPress are pm (the process manager mode), pm.max_children, pm.start_servers, pm.min_spare_servers, pm.max_spare_servers, and pm.max_requests.
Choosing a process manager mode: dynamic, static, ondemand
The pm directive controls how PHP-FPM starts and stops worker processes. There are three modes:
pm = dynamic starts a set number of workers at boot (pm.start_servers) and then scales between pm.min_spare_servers and pm.max_spare_servers idle workers, never exceeding pm.max_children. This is the default and the right choice for most WordPress servers. It keeps a warm pool ready for traffic without wasting memory during quiet periods.
pm = static starts exactly pm.max_children workers at boot and keeps them running permanently. There is no scaling, no spawn delay, and no idle reaping. This mode delivers the lowest per-request latency because a worker is always ready. The trade-off is memory: every worker consumes RAM whether or not there are requests. Use static only on a dedicated server with predictable traffic and enough RAM to keep every worker resident at all times.
pm = ondemand starts with zero workers. Workers are spawned when a request arrives and reaped after pm.process_idle_timeout seconds of inactivity (default 10 seconds). This mode uses the least memory but has the highest cold-start latency. It is practical on shared hosting where dozens of sites share one server and most are idle at any given moment.
For a typical WordPress VPS or dedicated server, pm = dynamic is the recommended starting point.
How to calculate pm.max_children for your server
pm.max_children is the hard ceiling on simultaneous PHP workers. Set it too low and requests queue during traffic spikes (producing high TTFB). Set it too high and the server runs out of RAM, starts swapping, and becomes slower than a smaller pool would have been.
The formula is:
pm.max_children = (Total available RAM for PHP) / (Average RAM per worker)
Step 1 — find total available RAM for PHP
Start from the server's total RAM and subtract what the operating system, database, web server, Redis, and any other service uses. On a 4 GB VPS running MySQL, nginx, and Redis, a realistic starting estimate is:
4096 MB total
- 512 MB OS + buffers
- 1024 MB MySQL (check innodb_buffer_pool_size)
- 128 MB nginx
- 256 MB Redis (if used for object cache)
= 2176 MB available for PHP-FPM
Step 2 — find average RAM per worker
The most reliable method is to measure it on a running server. After the site has been serving traffic for at least an hour:
ps -eo rss,comm | grep php-fpm | grep -v master | awk '{sum+=$1; count++} END {printf "Average worker RSS: %.0f MB\n", sum/count/1024}'
If you cannot measure yet, use these starting estimates:
- WordPress without WooCommerce: 40–60 MB per worker
- WordPress with WooCommerce: 60–90 MB per worker
- WordPress with heavy plugins (page builders, membership, LMS): 80–120 MB per worker
Step 3 — divide
Using the example above (2176 MB available, 60 MB per worker):
2176 / 60 ≈ 36 workers
Round down to leave a safety margin. Setting pm.max_children = 30 for this server would be a reasonable starting point.
A common mistake: confusing memory_limit with worker RAM
PHP's memory_limit directive (set in php.ini) caps how much RAM a single PHP script is allowed to allocate at its peak. It is not the same as actual worker memory. A worker's resident set size (RSS) is its real memory footprint, and it is almost always smaller than memory_limit. A site with memory_limit = 256M does not mean each worker uses 256 MB. Measure RSS, not the limit.
Setting pm.start_servers, pm.min_spare_servers, pm.max_spare_servers
These three directives only apply when pm = dynamic.
pm.start_serversis the number of workers spawned at FPM startup. A good default is the midpoint betweenpm.min_spare_serversandpm.max_spare_servers.pm.min_spare_serversis the minimum number of idle workers the pool keeps warm. If idle workers drop below this number, FPM spawns new ones. Set it to handle baseline traffic without queueing. For a VPS with moderate traffic, 25–30% ofpm.max_childrenis a reasonable starting point.pm.max_spare_serversis the maximum number of idle workers before FPM starts killing them to free memory. Set it slightly abovepm.min_spare_servers. There is no benefit to setting it equal topm.max_children— that would be equivalent to usingpm = static.
Example for pm.max_children = 30:
pm = dynamic
pm.max_children = 30
pm.start_servers = 10
pm.min_spare_servers = 8
pm.max_spare_servers = 15
pm.max_requests: preventing memory leaks
pm.max_requests sets the number of requests a single worker handles before it is killed and respawned. This prevents memory leaks from accumulating over a worker's lifetime. Poorly-coded plugins or themes can leak small amounts of memory on every request; without recycling, workers slowly grow until they exhaust the server.
A value of 0 means the worker lives forever (no recycling). That is the default and it is risky on WordPress sites with many plugins.
Set pm.max_requests between 500 and 1000. Lower values recycle more aggressively (more overhead from respawning), higher values recycle less (more risk of accumulated leaks). For most WordPress sites, pm.max_requests = 500 is a safe starting value.
pm.max_requests = 500
Reading the PHP-FPM status page and error log
The error log
PHP-FPM writes pool events to its error log (typically /var/log/php8.x-fpm.log on Debian/Ubuntu). The most important line to watch for is:
WARNING: [pool www] server reached pm.max_children setting (30), consider raising it
This line means every worker was busy at the same time and at least one request had to wait. If it appears occasionally during traffic peaks it is informational. If it appears continuously, the pool is undersized for the workload.
The status page
PHP-FPM has a built-in status endpoint. Enable it by adding to the pool configuration:
pm.status_path = /fpm-status
Then configure nginx to serve it locally:
location = /fpm-status {
allow 127.0.0.1;
deny all;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
After reloading both FPM and nginx:
curl http://127.0.0.1/fpm-status
The output includes active processes (workers currently handling a request), idle processes, total processes, max active processes (the high-water mark since FPM started), and max children reached (the number of times the pool ceiling was hit). These numbers tell you whether pm.max_children is sized correctly: if max active processes regularly equals pm.max_children, the pool is too small.
Worked example: 4 GB VPS running WordPress + WooCommerce
Server: 4 GB RAM, 2 vCPUs, Debian 12, PHP 8.3, nginx, MariaDB, Redis object cache.
Step 1 — available RAM for PHP:
4096 MB total
- 400 MB OS
- 1024 MB MariaDB (innodb_buffer_pool_size = 768M + overhead)
- 100 MB nginx
- 256 MB Redis
= 2316 MB available for PHP-FPM
Step 2 — measure worker RSS:
After running for a day, ps reports an average of 72 MB per worker (WooCommerce with 25 active plugins).
Step 3 — calculate:
2316 / 72 ≈ 32 workers
Round down to 28 to leave headroom for memory spikes.
Pool configuration (/etc/php/8.3/fpm/pool.d/www.conf):
[www]
pm = dynamic
pm.max_children = 28
pm.start_servers = 8
pm.min_spare_servers = 6
pm.max_spare_servers = 12
pm.max_requests = 500
pm.status_path = /fpm-status
Reload FPM:
sudo systemctl reload php8.3-fpm
Verify: monitor /var/log/php8.3-fpm.log for pm.max_children warnings over the next 48 hours. If the warning never appears and max active processes in /fpm-status stays well below 28, the pool is right-sized. If the warning fires frequently, either raise pm.max_children (if RAM allows) or reduce worker memory by auditing plugins and enabling OPcache tuning.