Metrics and alerting
Metrics and alerting
This page is for the person wiring fpm-tune into Prometheus or a dashboard. It lists every series the daemon publishes, the three alerts that matter, and the two other endpoints on the same address.
The daemon serves /metrics, /healthz and /history.json on one address: 127.0.0.1:9110 for the installed service, :9110 for a hand-run serve (--metrics, or metrics in the config; empty disables all three). None of them has authentication, and /history.json carries pool names and a day of per-pool load, so keep the address on loopback or a private network and reach it over an SSH tunnel. /healthz answers 200 ok while the process is up and says nothing more; fpm_tune_last_run_timestamp_seconds is what says the loop is running.
The series are named fpm_tune_*, disjoint from fpm-exporter's phpfpm_*, so both can be scraped side by side.
The three alerts that matter
The host is out of capacity. fpm_tune_capacity_exhausted is 1 when a pool wants more workers and there is nowhere to get them: no free budget, no neighbour holding slack. fpm_tune_pool_demand_unmet{pool} is the same news per pool. No configuration change will help; the host needs more memory or fewer pools. See Dividing the budget.
It was asked to apply and cannot. fpm_tune_apply_enabled says what the process was asked to do and never changes. fpm_tune_apply_blocked{reason} is 1 when a round declined to apply for a reason other than having nothing to do, and the reason is the fix:
no_master: no php-fpm master to apply to.lock: another fpm-tune holds the pool directory.unrepaired: a previous run left a change this one could not finish or undo. See Recovering a host.budget_unconfirmed: php-fpm's own memory limit could not be read, so the daemon refuses to write from the host's. Pass--memory, or make the master's cgroup readable. See The budget.state_unsaved: a change was applied but the record of it could not be written, so the reload brake is not on disk.
The last apply is old while a change is pending. fpm_tune_last_apply_timestamp_seconds advances only when a change was written and adopted. A daemon in apply mode whose fpm_tune_pool_workers_recommended has differed from fpm_tune_pool_workers_configured for longer than the hysteresis holds a change (20 minutes for a shrink) and whose last apply is older than that is not acting. fpm_tune_last_run_timestamp_seconds not advancing is a different fault: the loop has stalled.
Beside these, alert on any increase of fpm_tune_rollback_failed_total: a rejected file is still on disk, and the next reload from any source adopts it.
Every series
Per pool, labelled pool:
fpm_tune_pool_workers_configured:pm.max_childrenas php-fpm is running it.fpm_tune_pool_workers_recommended: the ceiling fpm-tune would set. Differs from configured while a change is pending or held back.fpm_tune_pool_worker_rss_bytes{estimate}: per-worker memory, PSS where the kernel reports it.typical_peakis what sizing follows,high_waterthe largest worker ever seen,p50,p95andp99the measured spread.fpm_tune_pool_subtree_rss_bytes: the high-water of a worker and everything it spawned. The gap tohigh_wateris what children cost.fpm_tune_pool_child_rss_bytes: the child memory folded into each worker's cost. Zero for a plain web pool.fpm_tune_pool_baseline_confidence: 0 to 1. Below 1 the pool is not cut below its configured ceiling.fpm_tune_pool_measured: 1 when the pool is sized from its own memory rather than an estimate.fpm_tune_pool_demand_unmet: 1 when the pool wanted more workers than it was given.fpm_tune_pool_cpu_ratio{estimate}: CPU seconds over wall seconds per request,p50andp90. Absent until the pool has enough readings.fpm_tune_pool_cpu_readings: how many requests the CPU share is built on. Under 20, no shape is called.fpm_tune_pool_cpu_fill_workers: how many busy workers of this pool fill the host's CPU.fpm_tune_pool_cpu_box_millicores_per_worker: what one busy worker costs the whole host, MySQL, nginx and the kernel included. Absent until the fit has enough spread.fpm_tune_pool_cpu_ceiling: the workers--cpuholds the pool at, published whether or not--cpuis on.fpm_tune_pool_cpu_starved_rounds: scrapes that found requests queued while the host's CPU was full.fpm_tune_pool_cpu_limited: 1 when the pool runs out of CPU before its memory ceiling.
The host:
fpm_tune_budget_bytes{state}:total,reserved,reserved_children,allocated,free.fpm_tune_cgroup_memory_bytes{state}: what the master's cgroup used,currentandpeak, workers and children together. Absent on a host with no cgroup.fpm_tune_capacity_exhausted: 1 when the host is out of capacity.fpm_tune_pools_unreachable: pools that could not be scraped this round. Their allocation is left alone.fpm_tune_pools_ambiguous: pool names shared by more than one master. Those pools are not published at all; name a master with--drop-in-dir. See The trust boundary.fpm_tune_last_run_timestamp_seconds: when the last round completed.
Acting:
fpm_tune_apply_enabled: 1 when this process acts on its plan.fpm_tune_apply_blocked{reason}: 1 when it could not, with the reason above.fpm_tune_last_apply_timestamp_seconds: when a change was last written and adopted.fpm_tune_applies_failed_total: applies that ended in an error.fpm_tune_rollbacks_total: changes taken back out because php-fpm rejected them or the master did not survive the reload.fpm_tune_rollback_failed_total: rejected changes that could not be taken back out.fpm_tune_repairs_total: times a previous run's leftovers had to be undone or completed, or the tool's own file removed to let php-fpm start.
A slice of a real scrape, from an apply-mode daemon:
fpm_tune_apply_enabled 1
fpm_tune_budget_bytes{state="allocated"} 1.44595331e+09
fpm_tune_budget_bytes{state="free"} 2.304453244e+09
fpm_tune_budget_bytes{state="reserved"} 4.355561062e+09
fpm_tune_budget_bytes{state="reserved_children"} 385500
fpm_tune_budget_bytes{state="total"} 8.105967616e+09
fpm_tune_capacity_exhausted 0
fpm_tune_pool_cpu_ceiling{pool="www-forge"} 10
fpm_tune_pool_cpu_limited{pool="www-forge"} 1
fpm_tune_pool_workers_configured{pool="www-forge"} 10
fpm_tune_pool_workers_recommended{pool="www-forge"} 10
/history.json
Every round leaves one sample in a ring in the daemon's memory, and every apply and every change it notices leaves an event. /history.json hands them out, and is what fpm-tune top draws from:
curl -s 127.0.0.1:9110/history.json | jq '.rounds[-1]'
curl -s '127.0.0.1:9110/history.json?last=120' | jq '.rounds[].pools[] | select(.pool=="www") | .active'
?last=N limits the rounds to the newest N; the events always come whole. The ring holds a day at the default interval (history in the config, --history on serve; 24 hours divided by the interval, and a thousand events). It starts empty at every daemon start and is never written to disk; Prometheus is the place for anything that must outlive a restart.
The shape, with one round from a real host:
{
"interval_seconds": 30,
"capacity": 2880,
"host": {
"hostname": "cbox-web",
"version": "1.0.0",
"apply": true,
"cpu_ceiling": true,
"cpu_headroom": 2,
"memory_bytes": 8105967616,
"cpu_millicores": 4000,
"memory_source": "/proc/meminfo"
},
"rounds": [
{
"at": "2026-09-03T19:54:24.257136064Z",
"host_busy_ratio": 0.07414469906368805,
"host_busy_known": true,
"pools": [
{
"pool": "www-forge",
"active": 1,
"queue": 0,
"configured": 10,
"recommended": 10,
"demand_unmet": false,
"worker_bytes": 43932035,
"memory_ceiling": 41,
"cpu_ratio_p50": 0.9,
"cpu_readings": 1464,
"cpu_fill_workers": 5,
"cpu_ceiling": 10,
"cpu_limited": true,
"cpu_bound": true
}
]
}
],
"events": []
}
host is what the title bar of top shows: the mode (apply), whether the CPU ceiling is on, and where the budget came from. A round carries the host's CPU busy ratio over the interval (host_busy_known is false on the first round) and, per pool, what was observed (active, queue, configured), what was planned (recommended, demand_unmet, worker_bytes, and memory_ceiling, what memory alone would have set, so a CPU-held pool shows the gap) and the CPU side.
Events carry at, kind, and where they apply pool, from, to and detail. The kinds: resized (the daemon changed a ceiling), apply_failed, rolled_back, rollback_failed, repaired, and changed, a ceiling that moved without the daemon moving it: a hand edit, a deploy, or an fpm-tune apply run beside it.