A PHP-FPM graceful reload (kill -USR2 <master_pid>, systemctl reload php8.3-fpm) is not graceful in the nginx sense. There is no overlap window where new workers handle fresh traffic while old workers drain. The old pool is torn down first, the master re-execs itself, and only then does it fork replacement workers. For a measurable interval zero workers are serving requests.
If you page on a 502 spike, a listen-queue jump, or a failed ping probe during a deploy, you have probably already met this window. The pattern is narrow, predictable, and bounded by process_control_timeout. Treating it as an incident is one of the most common PHP-FPM false alarms.
What it is and why it matters
SIGUSR2 is the only signal that triggers a true graceful reload of PHP-FPM. SIGUSR1 reopens log files only and never touches workers. SIGHUP is not handled and defaults to OS termination.
When SIGUSR2 arrives, the master does the following, in order:
- Send SIGQUIT to every worker, asking them to finish the current request and exit.
- Wait up to
process_control_timeoutseconds. - Escalate to SIGTERM for any worker still alive.
- Escalate to SIGKILL for any worker still alive after that.
- Re-exec itself via
execvp(), re-reading its configuration. - Fork new workers from the freshly re-exec’d master.
Step 5 is the part most operators miss. nginx’s reload model spawns new workers first, then drains the old pool in parallel. PHP-FPM inverts this: drain first, then re-exec, then fork. There is no overlap, and there is a hard moment in the lifecycle where no workers are bound to the FastCGI socket.
The listen socket file descriptor is preserved across execvp() and inherited by the new master. Connections that arrive during the window are not refused outright; they accumulate in the kernel listen backlog and are accepted once new workers come up. If the backlog fills first, the kernel drops them and the web server sees connection failures (502).
How it works
The diagram below shows the reload sequence. The horizontal gap between “old workers gone” and “new workers accept()ing” is the no-worker window.
sequenceDiagram
participant Op as Operator / systemd
participant M as Master
participant OW as Old workers
participant NW as New workers
participant K as Kernel listen backlog
Op->>M: SIGUSR2
M->>OW: SIGQUIT
Note over M,OW: wait process_control_timeout
(default 0)
M->>OW: SIGTERM (still alive)
M->>OW: SIGKILL (still alive)
Note over M: zero workers bound
K->>K: new conns pile in backlog
M->>M: execvp() re-exec master
(listen fd inherited)
M->>NW: fork workers
NW->>K: accept() queued connsThe width of the no-worker window is roughly:
window = process_control_timeout
+ time to drain workers that ignore SIGQUIT
+ execvp() cost (config parse, pool init)
+ fork + PHP runtime init for the first workers
With the default process_control_timeout = 0, workers that do not exit on SIGQUIT are killed immediately on the next tick, so the drain component collapses. The remaining components (execvp, fork, runtime init) are usually well under a second on a warm box, but they are non-zero. Under memory pressure, on a slow filesystem, or with many pools, the window stretches.
Two configuration values dominate the window:
process_control_timeout: the grace period granted to workers before they are force-killed. Accepts unit suffixes (s,m,h,d); a bare integer is treated as seconds. Match it tomax_execution_timeand you defeat the purpose; keep it small.listen.backlog: the kernel queue that absorbs connections during the window. Defaults vary by PHP version and platform; on modern Linux it is effectively clamped tonet.core.somaxconn.
Where it shows up in production
The no-worker window is most visible during deploys, logrotate runs, and config pushes. Common surfaces:
systemctl reload php*-fpmin a deploy pipeline. The reload is fast but not instantaneous; any in-flight or arriving requests during the window queue or fail.- logrotate reloads. A
postrotatehook that sends SIGUSR2 (instead of SIGUSR1, which only reopens logs) triggers a full pool recycle on every rotation. This is a classic misconfiguration. SIGUSR1 is the right signal for log rotation. - Config-only changes shipped via reload. These cost the same window as a code deploy.
- Multiple reloads in rapid succession, e.g. a deploy tool that fires SIGUSR2 in a loop on health check failure. On PHP < 7.4 this could crash the master entirely (PHP bug #74083). On modern PHP the master is protected by signal masking around
execvp(), but back-to-back reloads still amplify the no-worker window.
The window is also visible at the web server edge. nginx logs show connect() failed (11: Resource temporarily unavailable), upstream prematurely closed, or plain Connection refused during the window, all stamped within the same second or two as the reload.
When this matters (and how to bound it)
For most traffic patterns a sub-second no-worker window hidden behind a deep backlog is invisible. The window becomes a problem when any of these are true:
- Your web server has a tight
fastcgi_connect_timeoutand treats a stalled accept as a 502 before the new workers come up. - Your traffic arrival rate is high enough to fill the listen backlog during the window.
- You have workers that ignore SIGQUIT and run with
max_execution_time = 0, which can hold the master in the drain phase. - Your monitoring treats any ping failure or 502 as a page.
Mitigations, roughly in order of cost:
- Set
process_control_timeoutdeliberately. Default 0 minimises the drain component but kills workers mid-request the moment they ignore SIGQUIT. A small value (1-2 seconds) gives in-flight requests a chance to finish without stretching the window much. - Make sure logrotate uses SIGUSR1, not SIGUSR2. SIGUSR1 reopens logs without touching workers. SIGUSR2 is a full reload.
- Avoid stacking reloads. One reload per deploy. A deploy tool retrying reloads on health-check failure is papering over a different problem.
- Suppress availability alerts for ~120 seconds after an intentional reload. This is the single most effective noise reduction. Apply it to ping, 502 rate, and listen queue alerts, keyed to a reload or restart event.
- Do not assume reload clears OPcache. Whether the OPcache shared memory segment survives
execvp()depends on the PHP build and shared memory backend. Verify empirically withopcache_get_status()before and after a reload before tuning your deploy around either behaviour.
Signals to watch in production
These are the signals that move during a normal reload. Knowing their expected shape during the window is what separates “expected transient” from “real incident.”
| Signal | Why it matters during reload | Expected shape vs. red flag |
|---|---|---|
| Active processes | Drops to 0 as workers drain | Brief zero, then climbs back. Sustained zero after 120s is a red flag. |
| Total processes | Hits 0 (or just the master) during the window | Brief dip, then restoration. If it never climbs, new workers are not spawning. |
| Listen queue depth | Absorbs arrivals while no workers can accept | Brief spike that drains as workers come up. Sustained growth past the window is saturation, not reload. |
| Pool ping response | Ping queues behind the kernel backlog and may fail or stall | Brief failure or elevated latency. Persistent failure past 120s means the master did not come back. |
| Accepted connections rate | Drops during the window, then recovers | Brief dip is expected. No recovery means workers are not accepting. |
| Web server 502/504 rate | Spikes as connections fail or stall | Tight spike around the reload timestamp. Persistent elevation is a different incident. |
| PHP-FPM error log | Logs NOTICE: Reloading ... and NOTICE: ready to handle connections | Reload lines bracket the window. Repeated Reloading lines without ready mean reloads are stacking. |
The 120-second suppression window applies to the alerting signals above (ping, 502 rate, listen queue). Outside that window, every one of these signals means a real problem.
How Netdata helps
- Per-second polling of PHP-FPM status fields catches the actual shape of the no-worker window. Ten-second polling will often miss the dip and recovery entirely, leaving you with an alert and no graph.
- Correlating active processes, listen queue, and accepted connections on a single timeline makes the difference between “expected reload transient” and “real saturation” obvious at a glance.
- Annotated reload events (from the master’s
NOTICE: Reloadinglog lines) overlaid on the worker count chart let you confirm that the 502 spike and the reload share a timestamp. - ML anomaly detection on ping latency and 502 rate distinguishes a bounded reload-shaped spike from an open-ended anomaly.
- Web server upstream error metrics alongside the PHP-FPM pool view let you confirm the failure is on the FPM socket and not the web server itself.
Related guides
- PHP-FPM 504 Gateway Timeout: requests accepted but never finishing in time
- PHP-FPM active processes near max_children: reading pool utilization
- PHP-FPM in containers: cgroup limits and the silent OOM kill
- PHP-FPM “child N exited on signal 11 (SIGSEGV)”: worker segfaults
- PHP-FPM crash loop and fork storm: workers dying faster than they serve
- 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 “server reached pm.max_children setting (N), consider raising it”
- PHP-FPM pm.max_requests: worker recycling as the memory-leak safety net
- PHP-FPM memory leak: per-worker RSS climbing until the box runs out






