Panellicense

Tune PHP-FPM pools on cPanel — sizing pm.max_children

Size PHP-FPM pools per cPanel account without overcommitting RAM — pm.max_children math, dynamic vs ondemand vs static, and the failures that show up at 1 a.m.

8 min readUpdated 2026-05-16cpanel · php-fpm · performance · tuning
schema: HowToschema: FAQPageschema: BreadcrumbList

PHP-FPM on cPanel ships with a single global default that is wrong for almost every server that ever runs it. Out of the box, every account gets a pool with pm = ondemand and pm.max_children = 5 — which is fine for a marketing landing page and catastrophic for a WooCommerce store on Black Friday. The first time a busy site hits the wall you see 502 Bad Gateway in the access log, the pool spawns more workers than the box has RAM for, and the OOM killer takes down something unrelated.

This guide shows how to size pools correctly per account, when to switch process manager modes, and how to verify the numbers under real load. It assumes you're running EasyApache 4 with PHP-FPM enabled (the default since EA4 11.96) and have WHM root access.

Where pool configs actually live

cPanel manages FPM pools through three layers, in this order of precedence:

  1. Per-domain overrides/var/cpanel/userdata/<user>/<domain>.php-fpm.yaml. Created when you click MultiPHP Manager → PHP-FPM on a domain and edit values.
  2. Global FPM defaults/var/cpanel/ApachePHPFPM/system_pool_defaults.yaml. Applied to every newly-created pool. Edit once, rebuild, done.
  3. Pool template/var/cpanel/ApachePHPFPM/system.yaml. Rarely touched; controls the skeleton the per-domain files inherit from.

After any edit to the YAML files you must rebuild the Apache config and restart FPM:

/usr/local/cpanel/scripts/php_fpm_config --rebuild
/usr/local/cpanel/scripts/restartsrv_apache_php_fpm

Editing the /opt/cpanel/ea-php<XX>/root/etc/php-fpm.d/*.conf files directly works for about 20 minutes — until cPanel regenerates them from the YAML and your changes vanish. Always edit the YAML.

The memory math

pm.max_children is the only setting that matters for capacity planning. Everything else is secondary. The formula is the same one nginx blog posts have repeated for a decade, but it keeps being wrong in real configs because people forget the per-pool multiplier on a shared host.

pm.max_children (per pool) = (RAM available to PHP) / (avg worker RSS) / (active pools)

Concrete worked example. A 16 GB cPanel box with 200 hosted accounts:

  • Reserve 4 GB for MySQL, 1 GB for Apache/LSWS, 1 GB for the kernel and everything else.
  • That leaves 10 GB for PHP.
  • Average WordPress worker RSS with OPcache warm is 80–120 MB. Use 100 MB.
  • Of the 200 accounts, maybe 30 are actively serving traffic at any given minute. The other 170 are idle pools using zero RAM on ondemand.
  • 10 GB / 100 MB / 30 = ~3.4 children per active pool.

That gives each active pool a ceiling of 3 concurrent PHP requests. For a low-traffic shared host this is correct. For a host running a few traffic-heavy stores it is wrong by an order of magnitude — those stores need 20–30 workers and the rest of the box needs to yield to them.

The answer is not to set a higher global default. It is to tier accounts.

Tier accounts by workload, not by package

Pick three tiers and assign pool sizes per tier. A starting point:

TierAccount profilepm modepm.max_childrenMemory ceiling
IdleBrochure sites, parked domainsondemand5~500 MB peak
ActiveSmall WordPress, low-traffic Joomlaondemand10~1 GB peak
HeavyWooCommerce, LMS, busy forumdynamic30~3 GB peak

Apply with a per-domain YAML override:

# /var/cpanel/userdata/heavyuser/store.example.com.php-fpm.yaml
_is_present: 1
pm: dynamic
pm_max_children: 30
pm_start_servers: 4
pm_min_spare_servers: 2
pm_max_spare_servers: 6
pm_max_requests: 500

Then rebuild. The same file shape works for all three tiers — change pm and pm_max_children, leave the rest at defaults for dynamic pools.

For the idle and active tiers, stay on ondemand. It costs a few hundred microseconds per cold request but pays back hundreds of MB of RAM the box can give to the heavy tier during a spike. Do not use static on a multi-tenant cPanel server — it pins the full worker count in memory whether traffic is hitting the site or not.

If you're running CloudLinux with LVE limits, pair these tiers with matching LVE memory ceilings — pool size and LVE memory together form the contract. Without LVE, a runaway pm.max_children on one account can still starve neighbours.

pm.max_requests — the leak escape valve

Every PHP-FPM pool should set pm.max_requests to a non-zero value. Common defaults are 500–1000. The number tells the worker to die and respawn after handling that many requests, which reclaims memory leaked by long-running PHP extensions (looking at you, older ionCube, mongo, and any Composer package that caches reflection metadata).

Without it, every worker's RSS grows monotonically over a day. On a busy site you'll see the 100 MB average creep to 300 MB by 8 p.m. and your capacity math silently breaks.

pm_max_requests: 500

500 is a fine default. Drop to 200 for sites running known-leaky modules. Setting it to 0 (unlimited) is what the cPanel default does and what you should immediately change.

Verify the numbers under load

Don't trust your spreadsheet. Watch the actual RSS:

ps -ylC php-fpm --sort:rss | awk 'NR>1 {sum+=$8; n++} END {print "avg KB:", sum/n, "  count:", n}'

This prints the average resident size and worker count across every active FPM process on the box. Run it during a peak hour. If the average is double your planning assumption, your pm.max_children math is now wrong and the box is overcommitted — fix it before the next peak, not after.

For per-pool visibility, enable the status page on heavy-tier accounts:

pm_status_path: /fpm-status
ping_path: /fpm-ping

Then curl https://store.example.com/fpm-status?full from the server shows live worker counts, slow requests, and accepted-connection rates. Don't expose these endpoints publicly — restrict them in .htaccess to the server's own IP.

The four failure modes you'll actually hit

502 Bad Gateway, pool exhausted. FPM is queueing requests past pm.max_children and Apache's proxy timeout fires first. Symptom: bursty 502s during traffic peaks, no PHP error in the site log. Fix: raise pm.max_children on that account's pool, or shed traffic.

OOM killer eats MySQL. Total FPM RSS across pools exceeds free RAM, kernel kills the largest process — usually mysqld. Symptom: site is up, database is dead, /var/log/messages shows Out of memory: Killed process ... mysqld. Fix: lower pm.max_children ceilings or move heavy accounts to a smaller static-pool count with a hard cap.

Slow first request after idle. Pool is on ondemand with pm.process_idle_timeout = 10s and the worker has been reaped. Symptom: first request after a quiet minute takes 2–4 seconds, subsequent requests are fast. Fix: switch to dynamic with a small pm.min_spare_servers (1–2). This is the correct trade for any monetised site.

Workers stuck on slow MySQL queries. All pm.max_children workers are blocked on the same slow query, new requests queue. Symptom: fpm-status shows all workers in Reading or Running state for the same script. Fix: this isn't an FPM problem — find the query with mysqltuner or LVE statistics. Adding more FPM workers just kills MySQL faster.

When to skip FPM and use LSAPI instead

If you're already paying for a LiteSpeed license, LSAPI uses 5–15 MB per idle site versus FPM's 30–60 MB, and the workers are shared across the whole server rather than partitioned per account. For high-density shared hosting (500+ accounts on one box), the RAM savings alone usually pay for the LiteSpeed license inside two months.

The catch: LSAPI on Apache (mod_lsapi) needs CloudLinux. LSAPI on LSWS is the default and doesn't. See the handler comparison for the full breakdown.

What is a good pm.max_children value for WordPress on cPanel?+
Size by tier, not by CMS. A small WordPress site is fine at 10 with pm=ondemand. A busy WooCommerce store needs 20–30 with pm=dynamic. The constraint is per-worker RSS times concurrent active workers across all pools — not the CMS itself.
Why does pm.max_children only allow 5 by default in cPanel?+
cPanel sets the conservative ondemand default of 5 in /var/cpanel/ApachePHPFPM/system_pool_defaults.yaml so a single misconfigured account can't take down a fresh server. Change it there for new pools, or override per domain under MultiPHP Manager → PHP-FPM.
Should I use pm = static, dynamic, or ondemand?+
Ondemand for low-traffic and brochure sites — zero idle RAM cost. Dynamic for monetised sites that need a warm worker pool. Static is wrong on shared hosting because it pins all workers in RAM even when the site is idle. Use static only on dedicated single-tenant boxes.
Why are my PHP-FPM changes reverting after a few minutes?+
You edited /opt/cpanel/ea-php<XX>/root/etc/php-fpm.d/*.conf directly. cPanel regenerates those files from the YAML in /var/cpanel/ApachePHPFPM/ and /var/cpanel/userdata/. Edit the YAML and run /usr/local/cpanel/scripts/php_fpm_config --rebuild instead.
How do I see how many PHP-FPM workers are actually running?+
Run ps -ylC php-fpm to list every worker with its RSS, or enable the per-pool status page (pm.status_path) and curl it from the server. For aggregate totals during a peak, the ps command piped to awk gives you average RSS and worker count in one line.

Next steps

Switch in an afternoon

Switch from your current reseller — free.

We migrate active cPanel, Plesk, LiteSpeed and CloudLinux licenses from any reseller. We prorate the first month so you never pay twice, and your customers see zero downtime during the swap.