Skip to content

CPU per request

CPU per request

Memory sizing is the wrong answer for a pool whose requests compute. This page is what fpm-tune measures on the CPU side, what the CPU table in every plan means, and what --cpu changes. How the figures are collected is in the appendix.

What it measures

php-fpm's full status page reports, for each idle worker, the CPU share of the request it just finished: the fraction of its wall time spent on CPU, user plus system, with the CPU of anything it spawned counted in. 100% is a request that computed the whole time; 10% is one that mostly waited on the database. It comes with the page fpm-tune already fetches, and is recorded on every scrape.

The median share classifies the pool: cpu-bound at 50% and above, mixed from 20%, i/o-bound below. The median in millicores is what one busy worker costs in CPU (a 90% request is 900m while a worker is busy with it), the CPU twin of the per-worker memory cost.

Why memory still sizes

A request that computes for most of its wall time gets slower for every worker running beside it once the cores are full. Slower requests hold their workers longer, fewer are free, and the queue grows without draining. The memory budget saw none of it: there was room for eighty workers, and eight would have been faster.

Memory and CPU fail differently, though. Over-provisioning memory ends in the OOM killer; over-provisioning CPU ends in a slow site you can watch. So the CPU shape is reported in every plan, and holding a pool to it is a separate decision, off by default.

Children

php-fpm's share counts the children a request waited for: the ffmpeg behind an exec(), and whatever it spawned in turn. A transcode on eight cores for the whole request is a share of 800%, filed as such, so on a four-core host that pool fills its CPU at one busy worker. A child that outlives the request, backgrounded or daemonised, is not counted; its CPU is host pressure the plan does not read.

Host overhead

php-fpm's figure is what a request costs inside its worker, and says nothing about the MySQL query it waited on, the nginx that proxied it, or the kernel. So each scrape also reads the host's CPU time and each pool's workers' own, and fits one to the other over the natural spread of traffic. The slope is the host overhead: the cores the host spends for every core the pool's workers spend, 1.1 on a four-core Laravel host with MySQL beside it. The fit is believed at 30 points with 0.2 cores of spread; until then a worker is priced at PHP's own figure and the table says so.

Fill count and ceiling

The host's CPU in millicores divided by the per-worker cost, rounded up, is the fill count: how many of this pool's workers, all busy at once, fill the CPU. A half-core container divides as 500m and fills at one busy worker. Past the fill count another worker serves no more requests. The ceiling is ceil(fill × headroom), never below cores plus one, because a pool held at exactly the fill count has no worker to spare for a request stuck on an upstream, and short requests would wait behind long ones in the listen queue.

Headroom defaults to 2 (--cpu-headroom, or cpu-headroom in the service config) and takes a number from 1 to 100. Two is the generous end of what operators use by hand, because a worker too many costs a little load and a worker too few stalls a site. It is the one judgement on this page, and the table prints it beside the ceiling.

A pool can carry its own: env[FPM_TUNE_CPU_HEADROOM] = 3 in the pool's configuration, for a pool with a slow payment API behind it. The table marks such a ceiling (the pool's own). A marker that does not read as a number from 1 to 100 is a warning in the plan and in the recommendation file, naming the pool and the value, and the host's value is used for that pool.

Fill counts are per pool, each against the whole host, so they do not add up. The line under the table adds them: what every measured pool would draw at its ceiling, now and at this plan, against the CPU there is. It is a worst case, and the one line that can say the host as a whole is short of CPU.

What --cpu changes

Nothing about the measurement. Without it the plan sizes on memory, and the table's LIMIT column says which of memory and CPU each pool runs out of first: cpu when memory would allow more workers than the CPU ceiling. With --cpu such a pool is held at the CPU ceiling. From today's host:

POOL       MODE     NOW  PLAN  MEMORY    WHY
www-forge  dynamic  10   10    419.0MiB  cpu-bound; held at its CPU ceiling of 10 rather than the 41 memory allows (the CPU table has the fill count and headroom behind it), measured 41.9MiB/worker + 37.6KiB children

CPU per request, as measured:
  POOL       TYPICAL  P90  READINGS  PHP/WORKER  BOX/WORKER  LIMIT  WHY
  www-forge  90%      95%  1460      900m        995m        cpu    cpu-bound; ~5 busy workers fill 4 core(s) with MySQL, nginx and the kernel counted (1.1× PHP's own); ceiling 10 at 2× headroom; held there (now 10); queued in 4 rounds while the box was full

Five busy workers at 995m fill four cores; twice that is the ceiling of 10. The last clause counts scrapes that found requests queued while the host was at 95% or more of its CPU: another worker would not have helped.

The ceiling caps only what a pool wants, never its floor. A pool not yet watched long enough to be cut on memory evidence keeps its configured ceiling, because a cap below it is a cut, and the CPU path waits for the same confidence as the memory path.

Turning it on

fpm-tune plan --cpu                    # see what the ceiling would do
fpm-tune serve --cpu                   # advisory, with the ceiling binding in the recommendation
sudo fpm-tune install-service --cpu    # the installed service; re-run with --cpu=false to switch back

install-service --cpu writes cpu = true to /etc/fpm-tune/config. The fpm_tune_pool_cpu_* series on /metrics (see metrics and alerting) and the cpu per request line in the recommendation file are there either way.