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:

ModeExpected total processesWhen a shortfall is abnormal
staticAlways equal to pm.max_childrenAny value below pm.max_children
dynamicBetween pm.min_spare_servers and pm.max_childrenBelow pm.min_spare_servers under load, or stuck and not scaling up
ondemand0 to pm.max_childrenWorkers 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

CauseWhat it looks likeFirst thing to check
Fork blocked by system limitTotal 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 loopTotal fluctuating rapidly, “exited on signal” entries in error logFPM error log for signal numbers; dmesg for segfault or OOM detail
Stuck workers eroding capacityTotal below max_children, some workers showing extreme request durations, no exits loggedFull status page per-worker request duration field
process.max global ceilingTotal across all pools capped below the sum of their max_children valuesphp-fpm -tt output for the process.max directive
Memory exhaustionFork fails silently or workers OOM-killed, available memory near zerodmesg 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 upphp-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". If pm.max_children exceeds this rlimit, the master cannot fork enough workers. The rlimit comes from the systemd unit (LimitNPROC) or the shell that launched FPM. The pool config rlimit_files controls 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. Check cat /proc/sys/kernel/pid_max against ps -e | wc -l.
  • cgroup pids controller: on systemd hosts, the php-fpm service runs in a cgroup with a TasksMax limit. When the cgroup hits this limit, fork returns EAGAIN and the kernel logs cgroup: fork rejected by pids controller. PHP-FPM may log nothing useful. Check with systemctl 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

SignalWhy it mattersWarning sign
total processes (status page)Validates the pool is populated to the configured levelBelow pm.max_children in static mode, or below pm.min_spare_servers in dynamic mode under load
active + idle vs totalCross-check for scoreboard desyncSum 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 rlimitFork blocked by process rlimitLimit at or below pm.max_children
System PID count vs pid_maxFork blocked by kernel PID ceilingCurrent process count approaching /proc/sys/kernel/pid_max
TasksCurrent vs TasksMaxFork blocked by cgroup pids controller (systemd)TasksCurrent pinned at TasksMax
Available memoryFork blocked by OOM or cannot allocate worker memoryAvailable memory near zero; OOM entries in dmesg
process.max global settingGlobal ceiling across all poolsSet below the sum of all pool pm.max_children values
Per-worker request duration (full status)Detects stuck workers eroding effective capacityMultiple 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_timeout on 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 = yes on PHP 7.3+. Workers can hang in shutdown functions after fastcgi_finish_request(). This extends the timeout to cover that phase.
  • Set pm.max_requests to 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 LimitNPROC and TasksMax against pm.max_children. These limits are a common silent fork blocker on systemd-based hosts. Check them whenever you raise pm.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_threshold and emergency_restart_interval as 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, and idle processes per pool let you see the shortfall the moment it develops, not minutes later. In static mode, any deviation from pm.max_children is 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.