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:

  1. Send SIGQUIT to every worker, asking them to finish the current request and exit.
  2. Wait up to process_control_timeout seconds.
  3. Escalate to SIGTERM for any worker still alive.
  4. Escalate to SIGKILL for any worker still alive after that.
  5. Re-exec itself via execvp(), re-reading its configuration.
  6. 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 conns

The 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 to max_execution_time and 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 to net.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*-fpm in 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 postrotate hook 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_timeout and 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_timeout deliberately. 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 with opcache_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.”

SignalWhy it matters during reloadExpected shape vs. red flag
Active processesDrops to 0 as workers drainBrief zero, then climbs back. Sustained zero after 120s is a red flag.
Total processesHits 0 (or just the master) during the windowBrief dip, then restoration. If it never climbs, new workers are not spawning.
Listen queue depthAbsorbs arrivals while no workers can acceptBrief spike that drains as workers come up. Sustained growth past the window is saturation, not reload.
Pool ping responsePing queues behind the kernel backlog and may fail or stallBrief failure or elevated latency. Persistent failure past 120s means the master did not come back.
Accepted connections rateDrops during the window, then recoversBrief dip is expected. No recovery means workers are not accepting.
Web server 502/504 rateSpikes as connections fail or stallTight spike around the reload timestamp. Persistent elevation is a different incident.
PHP-FPM error logLogs NOTICE: Reloading ... and NOTICE: ready to handle connectionsReload 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: Reloading log 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.