FPM capacity sizing
BOA sizes PHP-FPM in two layers: a box-wide RAM budget derived once from
installed memory, and a per-pool pm.max_children that depends on the pool
type and (for tenant pools) the Octopus plan. This page is the authoritative
account of both, plus the override knobs that pin them.
Two pool families exist on a BOA host, sized by different code paths:
- Master / host pools
wwwNN(one per installed PHP version, in/opt/phpNN/etc/pool.d/wwwNN.conf) serve BOA's own system context. Sized from CPU only by_tune_web_server_config(lib/functions/php.sh.inc). Plan tiers never apply here. - Tenant pools
<oN>(per Octopus instance, in/opt/phpNN/etc/pool.d/<oN>.conf; one per installed FPM version when multi-version FPM is active) — shared by every site on that instance for a given version. Carry the plan-based model, applied both at Octopus install/upgrade (_satellite_tune_fpm_config,lib/functions/satellite.sh.inc) and by the live periodic agent (_satellite_tune_fpm_workers,aegir/tools/system/manage_ltd_users.sh). Both writers compute the same result, so neither clobbers the other on a later run.
All pools run pm = ondemand (fpm-pool-common*.conf,
pm.process_idle_timeout = 10s): workers spawn per request and are reaped 10s
after going idle. An idle version pool therefore consumes ~0 even with a high
pm.max_children, so the ceiling is a burst absorber, not a reservation.
The box-wide RAM budget
_php_budget_for_ram (lib/functions/system.sh.inc) derives the PHP memory
budget from installed RAM. The keying value is _USE, not raw RAM: usable RAM
has _RESERVED_RAM subtracted first (when _RESERVED_RAM is left at its default
0 it is auto-set to RAM/4), then _USE = usable-RAM / 4. _USE is mapped to
tiers that roughly double, each setting _USE_PHP (the FPM memory_limit
basis), _USE_OPC (opcache) and _USE_CLI (CLI memory_limit), all in MB:
_USE (MB) |
_USE_PHP |
_USE_OPC |
_USE_CLI |
|---|---|---|---|
| ≥ 16384 | 8192 | 8192 | 16384 |
| ≥ 8192 | 4096 | 4096 | 8192 |
| ≥ 2048 | 2048 | 2048 | 4096 |
| ≥ 512 | 1024 | 1024 | 2048 |
| < 512 | _USE |
_USE |
_USE |
The 16384 tier corresponds to roughly 85 GB of installed RAM.
On high-RAM VS and TG (bare-metal-class) nodes the budget is no longer
pinned at _USE_PHP=2048: the per-phpNN.ini memory_limit ceiling is lifted
from 64000 to 128000 (applied to every installed phpNN.ini) so these boxes
scale past the old cap. Two deliberate pins remain:
- Legacy OpenVZ — any virtualisation that is not XEN/AWS, at
_USE ≥ 2048, is held at_USE_PHP=_USE_OPC=_USE_CLI=512. OpenVZ OOM killing is aggressive and this path is untested, so the low ceiling is intentional — do not raise it without OpenVZ testing. _VMFAMILY=VZadditionally overrides_USE_OPC=64.
After the ladder and platform adjustments, two final floors apply, scaled by
total RAM so small VMs are not pinned to a 1 GB per-pool memory_limit — the
flat 1 GB floor caused OOM under even light concurrency on 4 GB / 2 CPU boxes:
| Total RAM | _USE_PHP floor |
_USE_FPM floor (memory_limit) |
|---|---|---|
| < 2 GB | 512 | 256 |
| 2–4 GB | 1024 | 512 |
| ≥ 4 GB | 1024 | 1024 |
Boxes with 4 GB or more are unchanged from the historical flat 1024 floor (including the OpenVZ pin and large-box sizing); only smaller VMs are lowered. A failed RAM read keeps the safe 1024 floor.
From this, the FPM-pool and cache shares are derived (_USE_FPM = _USE_PHP / 2,
RAM-scaled floor as above) — _USE_FPM is the box-wide FPM-pool
memory_limit budget, the value dedicated-plan pools inherit. The opcache and
APCu shares are covered in opcache & APCu.
_RESERVED_RAM itself is documented with the other host control variables in
Host control files & INI reference.
Master pool sizing (wwwNN)
_tune_web_server_config (lib/functions/php.sh.inc) sizes the master pools
from CPU only:
pm.max_children = _LIM_FPM × 2
where _LIM_FPM is _PHP_FPM_WORKERS, or — when _PHP_FPM_WORKERS=AUTO (the
default) — _CPU_NR × 4. On a hosted system the value is floored at 48 and
capped at 100; if the resulting _CHILD_MAX_FPM would fall below 8 the
writer pins pm.max_children = 8. Plan tiers never enter this calculation.
_PHP_FPM_DENY (the per-pool disable_functions list) is also stitched into
every wwwNN.conf here.
Tenant pool sizing (<oN>) — shared vs dedicated
_CLIENT_OPTION (per Octopus instance, in /root/.<oN>.octopus.cnf) selects one
of two policies. Both halves of the result — pm.max_children and the per-pool
memory_limit — are computed and written by _satellite_tune_fpm_config /
_satellite_tune_fpm_workers.
Plan (_CLIENT_OPTION) |
Class | pm.max_children |
memory_limit |
|---|---|---|---|
MONSTER / CLUSTER |
dedicated | dynamic (RAM + CPU) | box-wide _USE_FPM |
ULTRA |
dedicated | dynamic (RAM + CPU) | box-wide _USE_FPM |
PHANTOM |
dedicated | dynamic (RAM + CPU) | box-wide _USE_FPM |
POWER / BUS |
shared | 16 (8 × 2) |
768 MB band |
EDGE / AGAIN / SSD / CLASSIC |
shared | 4 (2 × 2) |
512 MB band |
MINI / MICRO / HEADSPACE |
shared | 2 (1 × 2) |
256 MB band |
QUIET |
shared | 8 (write-time floor; not doubled) | 256 MB band |
Shared plans (POWER and below)
Shared plans keep small, fixed, multi-tenant-safe limits — you cannot hand every
tenant on a packed box a large cap. The base worker count per plan
(_LIM_FPM: POWER/BUS 8, EDGE/AGAIN/SSD/CLASSIC 2, MINI/MICRO/QUIET/HEADSPACE 1)
is multiplied by _CLIENT_CORES ("engines", read from log/cores.txt when
present) before the final × 2, then capped at 100. The × 2 doubling runs for
every plan except QUIET: for QUIET the _CHILD_MAX_FPM = _LIM_FPM × 2 step
is skipped entirely, so _CHILD_MAX_FPM is never set on that path and the pool
writer falls back to its hard minimum — pm.max_children = 8 — at write time.
QUIET therefore lands on the 8-worker floor, not on 1 × 2.
The shared memory_limit is a restricted per-plan band — 768 / 512 / 256 MB —
capped at a RAM-scaled box ceiling (256 below 2 GB, 512 on 2–4 GB, 1024 at
≥ 4 GB) so a shared pool never exceeds box capacity on a small VM. Returning
empty (any non-shared plan) lets the pool inherit the box-wide value baked into
the common include.
Dedicated plans (PHANTOM and above)
Dedicated plans ignore the fixed tier entirely. The _LIM_FPM doubling still
runs as a fallback, but _php_fpm_apply_dynamic_children then overrides
pm.max_children for PHANTOM/ULTRA/MONSTER/CLUSTER (when workers are
AUTO) with a value sized from actual box capacity:
pm.max_children = min( _PHP_FPM_RAM_PCT% × total_RAM / _PHP_FPM_USS_MB ,
_CPU_NR × _PHP_FPM_CPU_FACTOR ) # floor 8
RAM is the primary axis: per-child private footprint (_PHP_FPM_USS_MB, measured
USS) defaults to 64 MB. CPU is a sanity cap — _PHP_FPM_CPU_FACTOR defaults
to 8 because FPM workers are I/O-bound, so more children than cores is
expected. _PHP_FPM_RAM_PCT defaults to 50 (% of total RAM). If the RAM read
fails the calc falls back to the CPU bound (_CPU_NR × _PHP_FPM_CPU_FACTOR),
never to the low floor.
Worked example: a 16-core / 62 GB host on the defaults gives
min(62000 × 50% / 64, 16 × 8) = min(484, 128) = 128. Dedicated pools keep the
generous box-wide memory_limit (_USE_FPM). The value is authoritative and
recomputed on every run — an upgrade never resets it back to a fixed tier.
When a pool saturates — and when raising the ceiling is wrong
All sites on an Octopus instance share the per-version tenant pool, so when
that pool hits pm.max_children every site on the instance queues behind the
same saturated pool — even idle ones — which is how one busy site surfaces as
failures on its neighbours. The symptom-side walk-through lives in
502 / 504 upstream failures;
this page owns the capacity decision. Dedicated plans absorb bursts by sizing
from box capacity; shared plans are capped on purpose, so a busy shared tenant
queueing at its ceiling is expected behaviour, not a fault.
Raising pm.max_children is not the cure when the cause is abusive
traffic — an attack consumes any ceiling, and a higher one only raises the OOM
point. BOA instead bounds the expensive anonymous-localized request class at
the nginx edge: a per-vhost cap of 24 in-flight anonymous localized requests
(provision drush option nginx_i18n_anon_conn, default 24; values below 1
fall back to the default), with the excess shed via limit_conn_status 444.
The guard is on by default for every vhost (the $boa_i18n_guard map defaults
to 1); opt a host out with a "host" 0; line in
/data/conf/boa_i18n_guard.map. The mechanics belong to
Request guards.
Two independent monitors tail /var/log/php/php*-fpm-error.log for the
authoritative per-pool exhaustion line server reached max_children setting,
each with per-file byte-offset tracking so only new hits since the last
run count:
- The minutely
php.shmonitor (_fpm_proc_max_detection, offsets in/var/log/boa/fpm_maxchildren.pos) emits the plain capacity-planning advisoryNOTE: PHP-FPM reached max_children setting (N new hit(s) in <files>). Consider raising pm.max_children for the affected pool(s)— a capacity signal only, no restart.php.shis part of theminute.shwatchdog fan-out — see Service auto-healing watchdogs. scan_nginx's FPM-saturation trigger (_NGINX_FPM_SAT_DETECT=YESby default) alerts and snapshots the box-wide top talkers as attack forensics. See scan_nginx scoring.
Rule of thumb: the NOTE without abuse forensics is legitimate growth — raise
capacity via the override knobs below. A sat-detect trip is abuse — reach for
the edge guardrails, not a higher ceiling. Either way, validate the configured
value first: fpmreport's per-pool table shows plan and eng (engines)
next to cfg_kids/cfg_mem and the observed peak/p95, so a configured
pm.max_children can be checked at a glance against its sizing inputs —
details in opcache & APCu.
Override knobs
All four are per-instance, written into /root/.<oN>.octopus.cnf, and default to
AUTO (fully dynamic):
| Variable | Effect |
|---|---|
_PHP_FPM_MAX_CHILDREN_FORCE |
Pin an exact pm.max_children for any plan (clamped to a minimum of 8); never clobbered |
_PHP_FPM_MEMORY_LIMIT_FORCE |
Pin an exact memory_limit in MB (minimum 64) for any plan |
_PHP_FPM_RAM_PCT |
Percent of total RAM budgeted for dedicated-plan workers (AUTO → 50) |
_PHP_FPM_CPU_FACTOR |
Dedicated-plan CPU cap multiplier: cores × factor (AUTO → 8) |
Precedence, highest first: _PHP_FPM_MAX_CHILDREN_FORCE overrides everything; a
numeric _PHP_FPM_WORKERS (not AUTO) is honoured as an explicit worker count
and takes precedence over the dynamic calc; otherwise the plan policy applies.
_PHP_FPM_USS_MB overrides the measured per-child footprint (default 64 MB) used
in the dynamic RAM axis.
Where the FPM configs live
Per-version FPM configs are deployed from boa-private/aegir/conf/php/:
aegir/conf/php/
php{56,70,71,72,73,74,80,81,82,83,84,85}-cli.ini
php{56,70,71,72,73,74,80,81,82,83,84,85}-fpm.conf
fpm{56,70,71,72,73,74,80,81,82,83,84,85}-pool-www.conf
fpm-pool-common.conf (shared fragment, default)
fpm-pool-common-legacy.conf (shared fragment, PHP 7.x / 5.6)
fpm-pool-common-modern.conf (shared fragment, PHP 8.x tenant pools)
Each version's fpmNN-pool-www.conf carries an include = line pointing at one
of three shared fpm-pool-common*.conf fragments deployed under
/opt/etc/fpm/. The master wwwNN pools include fpm-pool-common.conf (8.x) or
fpm-pool-common-legacy.conf (7.x / 5.6); tenant pools have the include rewritten
to fpm-pool-common-modern.conf on PHP 8.x and -legacy on 7.x. All three
fragments carry the same ondemand settings (pm.process_idle_timeout = 10s)
and the baked box-wide memory_limit (the 395M placeholder that
_tune_memory_limits rewrites to _USE_FPM) that dedicated pools inherit.
These common files are overwritten on barracuda upgrade — re-apply any
manual edits afterwards. The runtime FPM installer / version-switcher is
lib/functions/php.sh.inc (~3500 lines, one of the largest
lib/functions/*.sh.inc files after system.sh.inc and satellite.sh.inc).
Per-pool security: _PHP_FPM_DENY
Each PHP-FPM pool runs with a disable_functions list that blocks dangerous PHP
functions (exec, system, passthru, …). It is a two-level control with no
INI tier: the host-wide default is _PHP_FPM_DENY in /root/.barracuda.cnf
(read by _tune_web_server_config for the wwwNN master pools,
lib/functions/php.sh.inc), overridable per-Octopus by _PHP_FPM_DENY in
/root/.<oN>.octopus.cnf (read by _satellite_tune_fpm_config for that
instance's tenant pools, lib/functions/satellite.sh.inc). Both writers stitch
the value in by appending it to the pool's passthru, disable list. There is no
per-platform or per-site _PHP_FPM_DENY — neither boa_platform_control.ini
nor boa_site_control.ini carries the key, so the per-Octopus value is the
finest granularity. The security rationale lives in
Security & isolation.
Related
- opcache & APCu — the cache shares derived from this
budget (
_USE_OPC,_USE_APC) and theirphpNN.inisettings. - cache & cron tuning — diagnosing the FPM/APCu/Valkey failure modes this sizing prevents.
- Request guards — the nginx-edge anonymous-localized concurrency cap that keeps floods from consuming the shared pool.
- 502 / 504 upstream failures — the symptom-side walk-through of a saturated shared pool.
- Host control files & INI reference —
_RESERVED_RAM, INI precedence, the control/marker system. - Reference appendix — consolidated
_PHP_FPM_*and_USE_*variable table.