PHP-FPM tuning for WordPress: calculating pm.max_children and choosing a process manager mode

PHP-FPM pool settings decide how many PHP workers your server can run at once and how they are managed. This guide walks through the three process manager modes, the formula for calculating pm.max_children from available RAM, the supporting directives that control spare workers and recycling, and a worked example for a 4 GB VPS running WordPress with WooCommerce.

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_servers is the number of workers spawned at FPM startup. A good default is the midpoint between pm.min_spare_servers and pm.max_spare_servers.
  • pm.min_spare_servers is 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% of pm.max_children is a reasonable starting point.
  • pm.max_spare_servers is the maximum number of idle workers before FPM starts killing them to free memory. Set it slightly above pm.min_spare_servers. There is no benefit to setting it equal to pm.max_children — that would be equivalent to using pm = 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.

Done chasing slowdowns?

Performance issues tend to come back after quick fixes. WordPress maintenance keeps updates, caching and limits consistent.

See WordPress maintenance

Search this site

Start typing to search, or browse the knowledge base and blog.