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
The formula's inputs:
- RAM — the primary axis: per-child private footprint (
_PHP_FPM_USS_MB, measured USS) defaults to 64 MB. - CPU — a sanity cap:
_PHP_FPM_CPU_FACTORdefaults to 8 because FPM workers are I/O-bound, so more children than cores is expected. _PHP_FPM_RAM_PCTdefaults 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_FORCEoverrides everything;- a numeric
_PHP_FPM_WORKERS(notAUTO) 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).
Keeping an unused PHP version installed
The set of PHP versions installed on the host comes from _PHP_MULTI_INSTALL in
/root/.barracuda.cnf (_PHP_CLI_VERSION and _PHP_FPM_VERSION pick the CLI and
default-FPM versions; leave _PHP_SINGLE_INSTALL empty so the multi-install
variables apply). On a barracuda upgrade that set is rebuilt from what is
actually in use — the host scans every Octopus instance's
~/static/control/multi-fpm.info and keeps only the versions named there, so a
version you listed in _PHP_MULTI_INSTALL that no site uses yet is dropped from
the set on the next upgrade.
To keep an as-yet-unused version from being pruned, add a dummy line naming it to
any Octopus instance's ~/static/control/multi-fpm.info:
place.holder1.dont.remove 7.3
place.holder2.dont.remove 8.0
place.holder3.dont.remove 5.6
The version number at the end of each line is what the upgrade scan matches, so those versions survive. The same mechanism also protects versions already in use, re-adding them to the set automatically if they ever go missing.
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:
- Host-wide default —
_PHP_FPM_DENYin/root/.barracuda.cnf(read by_tune_web_server_configfor thewwwNNmaster pools,lib/functions/php.sh.inc). - Per-Octopus override —
_PHP_FPM_DENYin/root/.<oN>.octopus.cnf(read by_satellite_tune_fpm_configfor 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.