The ratio of active processes to pm.max_children is the most important saturation signal PHP-FPM exposes. Each worker handles one request at a time, so the active worker count is your current concurrent request load. Divided by the configured ceiling, it behaves like a capacity gauge: near 1.0 means no headroom; sustained near 1.0 means one slow query away from queuing.
This is a reading guide for that ratio: what the numerator and denominator actually mean, how to distinguish a normal burst from a sustained problem, how to use the high-water mark (max active processes) for capacity planning, and which correlated signals disambiguate “busy”, “saturated”, and “broken”. It is not a tuning guide; see the related guides at the end for that.
active processes counts workers currently executing a request. idle processes counts workers ready to accept one immediately. total processes is their sum. pm.max_children is the configured ceiling on how many workers the pool may run. The saturation ratio is active processes / pm.max_children. In static mode the denominator equals total processes; in dynamic and ondemand the master spawns workers up to that ceiling on demand.
What the ratio means
Each active worker is occupied for the full duration of a request, including time spent blocked on I/O. A worker waiting 4 seconds on a database query is “active” from PHP-FPM’s perspective even though it is consuming essentially zero CPU. This is why the active count can sit at the ceiling while CPU remains low: the workers are not computing, they are waiting.
| Ratio band | Interpretation |
|---|---|
| 0.0 to 0.7 sustained at peak | Healthy headroom. Bursts can be absorbed without queuing. |
| 0.7 to 0.85 sustained at peak | Tight. Capacity planning should be in motion. |
| Above 0.85 sustained | Danger zone. One slow dependency or traffic burst away from queuing. |
| 1.0 sustained | No headroom. Every new request waits for a worker to finish. |
1.0 plus non-zero listen queue | Confirmed exhaustion. Requests are stacking in the socket backlog. |
The boundary between “busy” and “saturated” is the listen queue, not the active count alone. Hitting 100% of pm.max_children means the next request will queue, but queuing is only observable once listen queue goes non-zero. Until then you are at the ceiling but the backlog is still absorbing the slack.
Burst versus sustained
A brief spike to 100% is normal. PHP-FPM is designed to absorb short bursts using the listen backlog as a shock absorber. A flash crowd, a cache invalidation, or a deploy-time opcache warmup can push active to the ceiling for a few seconds and then drain. This is not an incident.
Sustained near-ceiling is the problem. The rule of thumb: above 80% of pm.max_children sustained during normal traffic means insufficient headroom. The failure mode is cliff-edged, not gradual. Once the ceiling is reached, latency does not rise linearly. Requests stack in the socket backlog and time out in batches, so the transition from “fine” to “502s for everyone” can happen in seconds.
Sampling cadence matters. The status page is a point-in-time snapshot. A 10- or 30-second poll can miss a transient queue buildup entirely, showing listen queue = 0 while users saw intermittent 502s between polls. For operational alerting on saturation, poll at per-second intervals. For capacity planning, 10 seconds is adequate because you are tracking trends, not catching spikes.
The high-water mark: max active processes
active processes is the current snapshot. max active processes is the high-water mark since the pool (master) started: the peak concurrent active worker count observed over the lifetime of the process. For capacity planning it is usually more useful than the current value, because it tells you how close you have come to the wall even when things look calm right now.
Two readings matter:
- If
max active processesequalspm.max_children, the pool hit the ceiling at least once since start. This counter alone does not tell you whether requests queued, only that every worker was busy simultaneously at some point. Cross-checkmax listen queue: if it is also non-zero, queuing happened. - If
max active processessits well belowpm.max_children(for example, 60% at peak), you have headroom you may be able to reclaim. Oversized pools waste memory; the high-water mark tells you whetherpm.max_childrencan be lowered safely.
Both high-water counters reset on master restart. SIGUSR2 graceful reload re-execs the master, so the counters reset on every reload. If you reload frequently during deploys, the historical peak is less useful because you lose context. Track reloads alongside these counters, and note that accepted conn also resets on reload, which can produce artificial spikes or negative deltas in monitoring systems that compute rates across the boundary.
Correlations that disambiguate
The active ratio alone tells you utilization. It does not tell you why workers are busy, whether they are doing useful work, or whether users are already being harmed. Three correlations resolve almost every ambiguity.
flowchart TD
A[active / max_children] --> B{sustained above 0.8?}
B -- no --> C[healthy headroom]
B -- yes --> D{listen queue greater than 0?}
D -- no --> E[at ceiling, not yet queuing]
D -- yes --> F[confirmed exhaustion]
F --> G{CPU high or low?}
G -- low --> H[I/O-bound: slow log, DB, API, sessions]
G -- high --> I[compute-bound: more workers or more CPU]| Signal combination | What it means | First thing to check |
|---|---|---|
| High active, zero listen queue | At ceiling but backlog is absorbing it. Headroom is gone but users are not yet blocked. | Whether this is sustained or a burst. |
| High active, growing listen queue | Confirmed worker exhaustion. Requests are stacking. | Slow log and per-worker request URI to find what is holding workers. |
| High active, low CPU | Workers are blocked on I/O, not computing. Classic slow-dependency pattern. | Database, external API, NFS, DNS, session locks. Raising max_children may help only briefly. |
| High active, high CPU | Workers are genuinely computing. Compute-bound workload or opcache thrash. | Opcache hit rate and memory; whether more CPU or more workers is the right lever. |
| Active at ceiling, listen queue empty, web server returning 502 | Backlog already overflowed. Connections are being refused at the kernel level before reaching the queue. | Kernel ListenOverflows/ListenDrops counters (`nstat -az |
| Low active, high latency | A few workers running extremely slow code. Throughput is minimal despite spare-looking capacity. | Per-worker request duration from ?full status; slow log. |
The most common production pattern is high active plus low CPU. A slow database query, a hung external API call, or file-based session lock contention ties up workers for seconds or minutes while using no CPU. The pool looks saturated, but the root cause is upstream, not in PHP-FPM. Raising max_children in this case buys time but does not fix the problem, because the new workers will block on the same dependency.
Per-pool and per-mode reading
Read each pool independently. If you run multiple pools, aggregate active-process counts across pools are misleading. A memory leak or a slow endpoint in one pool does not affect the others, and one saturated pool behind a round-robin load balancer means every Nth request is slow even when aggregate utilization looks fine.
The process manager mode changes how you interpret the numbers:
static:total processesalways equalspm.max_children. The integrity checkactive + idle == max_childrenshould hold. Themax children reachedcounter is always 0 because the master never tries to spawn beyond the fixed pool, so do not alert on it. Watchlisten queueandmax active processesinstead.dynamic: Workers scale betweenpm.min_spare_serversandpm.max_children.max children reachedis meaningful: each increment is a moment the master wanted to spawn a worker but was blocked by the ceiling. Watch its rate of change, not the absolute value.ondemand: Workers spawn on request and die afterpm.process_idle_timeout.activeandtotalof 0 at idle is normal, not a failure. The cost is cold-start latency on the first request after idle, which can look like a brief saturation spike as workers fork and opcache warms.
Gotchas when reading the count
Counting bug under FastCGI keepalive. With nginx fastcgi_keep_conn on, the status page can report active processes and total processes far above pm.max_children (upstream reports show values like 3347 active against a 1500 ceiling). The active counter is incremented during the “reading headers” stage and decremented during the “accepting” stage, and with keepalive there is one accepting stage but multiple reading-headers stages. A fix was merged (PR #19191, July 2025). If you use fastcgi_keep_conn on and see active exceeding max_children, you cannot trust the ratio until you are on a patched build.
Status page polling consumes a worker. Each status request occupies a worker slot for the duration of the FastCGI request. Polling aggressively during a saturation event adds load to an already-loaded pool. Use the /ping endpoint for sub-second liveness (it returns a fixed string and does not require a worker slot in the same way) and reserve the full status page for 15-30 second monitoring polls or per-second polling only during active incidents. If you need the status page reachable even when the main pool is fully saturated, configure a separate status listener (pm.status_listen).
Status page exposure. The status endpoint reflects the request URI into its output. On unpatched versions, requesting ?html or ?xml output from a browser session with sensitive cookies can execute injected script (CVE-2026-6735, reported as patched in PHP 8.2.31, 8.3.31, 8.4.21, 8.5.6). Restrict the status path to localhost or trusted IPs regardless of version, and avoid loading it in a browser session that has access to the admin interface.
total processes below max_children in static mode. If total processes is less than pm.max_children in static mode, workers are dying faster than the master can replace them. This is a crash loop or a fork failure, not a capacity problem, and the active ratio becomes misleading because the effective ceiling is lower than configured.
Snapshot timing. The status page is a single point-in-time read. Between two polls an entire saturation event can occur and resolve. A current active of 40% does not rule out a spike to 100% two seconds ago. Always pair the current snapshot with the high-water mark (max active processes, max listen queue) to catch events that happened between polls.
Reading it manually
The numbers are available from the status page in text, JSON, or OpenMetrics format (OpenMetrics was added in PHP 8.1.0, enabling native Prometheus scraping without an exporter). The status page does not expose pm.max_children, so pull it from the pool config.
# Adjust path to match your installation. JSON field names use spaces,
# not underscores: "active processes", "max listen queue", etc.
POOL_CONF=/etc/php/8.3/fpm/pool.d/www.conf
MAX_CHILDREN=$(awk -F= '/^[[:space:]]*pm\.max_children[[:space:]]*=/{
gsub(/[[:space:]]/,"",$2); print $2; exit}' "$POOL_CONF")
curl -s "http://127.0.0.1/fpm-status?json" | python3 -c "
import sys, json
d = json.load(sys.stdin)
active = d['active processes']
ceiling = ${MAX_CHILDREN:-0}
ratio = active / ceiling if ceiling else 0
print(f\"active={active} max_children={ceiling} ratio={ratio:.2f}\")
print(f\"max active (high-water)={d['max active processes']}\")
print(f\"listen queue={d['listen queue']} (max seen={d['max listen queue']})\")"
Adjust the URL to match your pm.status_path and web server config. The defaults above assume the web server proxies /fpm-status to the pool. Using total processes in the denominator instead of pm.max_children is only correct in static mode; in dynamic and ondemand it overstates utilization whenever the pool is not full.
For the per-worker view that reveals which endpoints are holding workers, append &full and read request uri and request duration per process.
How Netdata helps
- Per-second sampling of
active processes,idle processes,total processes, andlisten queuecatches transient saturation events that 10- or 30-second polls miss entirely. - The active and total series sit next to CPU, request rate, and downstream dependency latency on one timeline, which is the disambiguation step that separates I/O-bound saturation from compute-bound saturation.
- Per-pool charts keep each pool’s saturation independent, so one noisy pool behind a load balancer does not get averaged away.
- Anomaly detection on the active series flags sustained-near-ceiling behavior as distinct from normal bursty traffic, reducing noise on brief spikes that drain on their own.






