The PHP-FPM status page reports total processes below pm.max_children, and the count is not climbing. Whether this is a problem depends on which process manager mode the pool runs and whether traffic is present.
In static mode, total processes must equal pm.max_children at all times. Any shortfall means workers have died and the master has not replaced them, or the master cannot fork new ones. This is always abnormal.
In dynamic mode, the count legitimately fluctuates between pm.min_spare_servers and pm.max_children. A low count during low traffic is expected. A low count under load, with idle processes stuck at zero and the listen queue building, means the pool cannot scale up.
In ondemand mode, zero workers at idle is the designed behavior. Workers spawn on request and die after pm.process_idle_timeout. Do not alert on total processes = 0 unless traffic is actively arriving and being rejected.
A genuine shortfall has two root shapes: the master cannot fork (system PID limits, cgroup pids controller, memory exhaustion, or the global process.max ceiling), or workers are leaving the pool faster than the master replaces them (crash loop, OOM kills). A third contributor is stuck workers that never return to idle: without request_terminate_timeout, they occupy slots indefinitely, and when they eventually die the pool must respawn them under pressure.
What this means
The total processes field on the status page is the sum of active processes and idle processes. It counts workers only, not the master. The expectation depends on the pm mode:
| Mode | Expected total processes | When a shortfall is abnormal |
|---|---|---|
static | Always equal to pm.max_children | Any value below pm.max_children |
dynamic | Between pm.min_spare_servers and pm.max_children | Below pm.min_spare_servers under load, or stuck and not scaling up |
ondemand | 0 to pm.max_children | Workers not spawning when requests are arriving and queueing |
If active processes + idle processes does not equal total processes in your monitoring, you have a collection timing issue (the status page is a point-in-time snapshot). If the OS process count from ps disagrees with the status page, the scoreboard and reality are out of sync, which can happen during rapid crash-respawn cycles.
The first question is always whether the master process is alive and responsive. If the master is hung or in an emergency restart loop, it will not spawn workers regardless of the configuration.
flowchart TD
A["total processes < max_children"] --> B{"Which pm mode?"}
B -->|"static"| C["Always abnormal"]
B -->|"dynamic, under load"| C
B -->|"dynamic, idle"| D["Expected variation"]
B -->|"ondemand, no traffic"| E["Expected: 0 workers"]
C --> F{"What does the error log show?"}
F -->|"child exited on signal"| G["Crash loop:
check dmesg and extensions"]
F -->|"fork rejected or EAGAIN"| H["System limit:
ulimit, pid_max, cgroup pids"]
F -->|"nothing obvious"| I["Check process.max
and stuck workers"]Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Fork blocked by system limit | Total stuck below target, no crash entries in FPM log, master alive and responsive | /proc/<master_pid>/limits for Max processes; dmesg or journal for fork rejection |
| Crash loop | Total fluctuating rapidly, “exited on signal” entries in error log | FPM error log for signal numbers; dmesg for segfault or OOM detail |
| Stuck workers eroding capacity | Total below max_children, some workers showing extreme request durations, no exits logged | Full status page per-worker request duration field |
| process.max global ceiling | Total across all pools capped below the sum of their max_children values | php-fpm -tt output for the process.max directive |
| Memory exhaustion | Fork fails silently or workers OOM-killed, available memory near zero | dmesg for “Out of memory”; system available memory |
| pm.max_spawn_rate too low (PHP 8.1+) | Dynamic pool ramps slowly under burst, total lags behind demand but eventually catches up | php-fpm -tt for pm.max_spawn_rate |
Quick checks
All read-only and safe to run during an incident.
# Confirm pm mode, max_children, and global limits from the parsed config
php-fpm -tt 2>&1 | grep -E "^pm|process.max|max_spawn_rate|max_children|min_spare|max_spare"
# Status page: total, active, idle, and the high-water counters
curl -s http://127.0.0.1/fpm-status
# Count actual worker processes from the OS (adjust binary name for your distro)
ps -eo pid,ppid,cmd | grep '[p]hp-fpm' | grep -v master | wc -l
# Check the master process's Max processes rlimit
cat /proc/$(pgrep -f "php-fpm: master" | head -1)/limits | grep "Max processes"
# Check kernel pid_max and current system-wide process count
cat /proc/sys/kernel/pid_max
ps -e --no-headers | wc -l
# Check systemd TasksMax for the php-fpm unit (if systemd-managed)
systemctl show php-fpm -p TasksMax -p TasksCurrent
# FPM error log: crash exits and any fork-related warnings
grep -iE "exited on signal|fork|Resource temporarily unavailable" /var/log/php-fpm/error.log | tail -30
# Kernel log: cgroup fork rejection, segfaults, OOM kills
dmesg -T | grep -iE "fork rejected|segfault|out of memory" | tail -20
The binary name and log path vary by distribution and PHP version: php-fpm, php-fpm8.1, php8.3-fpm, and so on. The error log may live at /var/log/php8.x-fpm.log instead of /var/log/php-fpm/error.log. Adjust the grep patterns and paths accordingly.
How to diagnose it
1. Confirm the pm mode and whether the shortfall is expected.
Run php-fpm -tt and check the pm directive for the affected pool. If the pool is ondemand and there is no active traffic, a total of zero is correct. If the pool is dynamic and traffic is low, a total near pm.min_spare_servers is correct. Only proceed if the mode is static, or if traffic is present and the pool should have more workers.
2. Compare the status page against the OS process table.
The status page total processes counts what the master thinks it has. The ps count is ground truth. If they disagree, the master’s scoreboard is stale, which points to a crash-respawn cycle or a master process problem. If they agree and both are below pm.max_children, the master genuinely has fewer workers than configured.
3. Check the FPM error log for worker exits.
Look for child <PID> exited on signal <N> entries. Signal 11 (SIGSEGV) and signal 7 (SIGBUS) indicate crashes, typically from extension bugs or memory corruption. Signal 9 (SIGKILL) indicates the OOM killer. Signal 6 (SIGABRT) indicates an assertion failure. If exits are rapid and frequent, you have a crash loop: workers die, the master respawns them, they hit the same code path, and die again.
4. Check for fork-blocking system limits.
If the error log shows no crashes but the total is still low, the master is likely unable to fork. Examine three layers:
- Master process rlimit:
cat /proc/<master_pid>/limits | grep "Max processes". Ifpm.max_childrenexceeds this rlimit, the master cannot fork enough workers. The rlimit comes from the systemd unit (LimitNPROC) or the shell that launched FPM. The pool configrlimit_filescontrols file descriptors, not process limits. - kernel.pid_max: if the system-wide process count approaches
pid_max, all fork calls on the host fail with EAGAIN. Checkcat /proc/sys/kernel/pid_maxagainstps -e | wc -l. - cgroup pids controller: on systemd hosts, the php-fpm service runs in a cgroup with a
TasksMaxlimit. When the cgroup hits this limit, fork returns EAGAIN and the kernel logscgroup: fork rejected by pids controller. PHP-FPM may log nothing useful. Check withsystemctl show php-fpm -p TasksMax -p TasksCurrent.
5. Check the global process.max directive.
process.max is set in php-fpm.conf (the global config, not per-pool). The default is 0 (unlimited). If set to a finite value, it caps the total number of processes FPM will fork across all pools combined. If you run multiple pools whose individual pm.max_children values sum to more than process.max, individual pools will be silently capped. Run php-fpm -tt and look for the process.max line.
6. Check for stuck workers.
Pull the full status page and look at per-worker request duration values (in microseconds). If several workers show durations far exceeding your application’s normal p99 (millions of microseconds when the baseline is tens of thousands), those workers are stuck on a slow backend, a hung connection, or an infinite loop. Without request_terminate_timeout, they will never be cleaned up. They remain alive and counted as active, reducing effective capacity. When they eventually crash or are OOM-killed, the total drops and the master must respawn under pressure.
# Full status: show running workers sorted by request duration (microseconds)
curl -s 'http://127.0.0.1/fpm-status?json&full' | python3 -c "
import sys, json
data = json.load(sys.stdin)
running = [p for p in data['processes'] if p['state'] == 'Running']
running.sort(key=lambda p: p['request duration'], reverse=True)
for p in running[:10]:
print(f\"PID {p['pid']}: {p['request duration']/1e6:.1f}s - {p['request uri']}\")"
7. Check for memory exhaustion.
If the host or cgroup is out of memory, fork fails or the OOM killer shoots workers. Check dmesg for “Out of memory” entries targeting php-fpm processes. In containers, the cgroup OOM killer can kill workers or the master silently. See the related guide on PHP-FPM in containers and the silent OOM kill.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
total processes (status page) | Validates the pool is populated to the configured level | Below pm.max_children in static mode, or below pm.min_spare_servers in dynamic mode under load |
active + idle vs total | Cross-check for scoreboard desync | Sum disagrees with total indicates timing issue or crash-respawn churn |
| Worker exit rate (error log) | Detects crash loop before it erodes the pool | “exited on signal 11” or “exited on signal 7” entries accumulating |
Master Max processes rlimit | Fork blocked by process rlimit | Limit at or below pm.max_children |
System PID count vs pid_max | Fork blocked by kernel PID ceiling | Current process count approaching /proc/sys/kernel/pid_max |
TasksCurrent vs TasksMax | Fork blocked by cgroup pids controller (systemd) | TasksCurrent pinned at TasksMax |
| Available memory | Fork blocked by OOM or cannot allocate worker memory | Available memory near zero; OOM entries in dmesg |
process.max global setting | Global ceiling across all pools | Set below the sum of all pool pm.max_children values |
Per-worker request duration (full status) | Detects stuck workers eroding effective capacity | Multiple workers at 10x or more the median duration |
Fixes
Fork blocked by system limit
Raise the limiting layer. If the master’s Max processes rlimit is the bottleneck, set LimitNPROC in the systemd unit and restart FPM. If kernel.pid_max is exhausted system-wide, raise it via sysctl kernel.pid_max=<higher_value> and persist in /etc/sysctl.conf. If the cgroup TasksMax is the ceiling, raise it in the systemd unit with TasksMax= (or TasksMax=infinity to remove the limit), run systemctl daemon-reload, then restart FPM.
These changes require a full restart, not a graceful reload (SIGUSR2). A reload re-execs the master but does not re-read systemd unit limits.
Crash loop
Identify the crashing code path from the per-worker request uri in the full status page and the signal number in the error log. If the crash is always on the same URI, block that endpoint at the web server level to protect the rest of the site while you debug. Check for recent PHP upgrades, extension version mismatches, or corrupted opcache shared memory. Setting emergency_restart_threshold and emergency_restart_interval provides a circuit breaker: if N children die within M seconds, the master restarts the entire pool rather than churning forever.
See the related guides on worker segfaults and crash loop diagnosis.
Stuck workers
Set request_terminate_timeout in the pool config (for example, request_terminate_timeout = 30). This kills any worker that exceeds the timeout and lets the master spawn a replacement immediately. On PHP 7.3+, also set request_terminate_timeout_track_finished = yes to cover workers that hang during the post-request shutdown phase (after fastcgi_finish_request()), which the base timeout does not cover by default.
For immediate relief during an incident, you can kill specific stuck workers with kill -SIGQUIT <pid>. SIGQUIT lets the worker finish its current response before exiting.
process.max global ceiling
If process.max is set and capping your pools, either raise it or set it to 0 (unlimited, the default). This directive is in php-fpm.conf, not in the pool config. A full restart is required.
Memory exhaustion
Free memory on the host or raise the cgroup memory limit. If workers themselves are consuming too much memory (per-worker RSS growing without bound), set pm.max_requests to 500-1000 to force periodic recycling. Do not raise pm.max_children to compensate for a memory problem: that makes the OOM condition arrive faster.
pm.max_spawn_rate too low (PHP 8.1+)
In dynamic mode under burst traffic, the master forks workers in batches limited by pm.max_spawn_rate (default 32, introduced in PHP 8.1). If the pool ramps too slowly to absorb bursts, raise this value. This affects ramp-up speed, not the steady-state total. See the related guide on dynamic mode scaling lag for detailed tuning.
Prevention
- Set
request_terminate_timeouton every production pool. Without it, a single hung request permanently removes a worker from effective service. Use 30-60 seconds depending on your application’s latency profile. - Set
request_terminate_timeout_track_finished = yeson PHP 7.3+. Workers can hang in shutdown functions afterfastcgi_finish_request(). This extends the timeout to cover that phase. - Set
pm.max_requeststo 500-1000. Forces periodic worker recycling, which bounds memory growth and gives the master regular fork exercise. A pool that never recycles workers will eventually hit memory pressure or silent capacity erosion. - Verify systemd
LimitNPROCandTasksMaxagainstpm.max_children. These limits are a common silent fork blocker on systemd-based hosts. Check them whenever you raisepm.max_children. - Monitor the FPM error log for “exited on signal” entries. A low, steady rate of segfaults indicates a latent bug that will become a crash loop under the right traffic pattern.
- Set
emergency_restart_thresholdandemergency_restart_intervalas a circuit breaker. Defaults are 0 (disabled). Without them, a crash loop churns indefinitely instead of triggering a clean pool restart.
How Netdata helps
- Per-second
total processes,active processes, andidle processesper pool let you see the shortfall the moment it develops, not minutes later. In static mode, any deviation frompm.max_childrenis immediately visible. - Correlating the FPM process count with system-level metrics (host process count, available memory, cgroup pids usage) pinpoints whether the shortfall is a PHP-FPM problem or a system-level fork blocker.
- Anomaly detection on worker-related metrics surfaces crash loops before they erode the pool below a usable level.
- Per-worker request duration from the full status page reveals stuck workers that are alive but functionally dead, explaining why effective capacity is lower than the process count suggests.
- Cgroup-level memory and pids metrics (in containerized deployments) catch the silent OOM kill and the cgroup pids controller fork rejection that PHP-FPM itself cannot log.
Related guides
- PHP-FPM crash loop and fork storm: workers dying faster than they serve
- PHP-FPM “child N exited on signal 11 (SIGSEGV)”: worker segfaults
- PHP-FPM in containers: cgroup limits and the silent OOM kill
- PHP-FPM dynamic mode scaling lag: why the pool cannot keep up with bursts
- PHP-FPM “server reached pm.max_children setting (N), consider raising it”
- PHP-FPM emergency restart: “failed processes threshold reached, initiating reload”
- How PHP-FPM actually works in production: a mental model for operators
- PHP-FPM idle processes at zero: no burst headroom left
- PHP-FPM listen queue growing: the earliest signal of saturation
- PHP-FPM active processes near max_children: reading pool utilization






