systemctl start php-fpm returns failed, the master process never appears in the process table, and the web server returns 502 for every PHP request. Workers are never forked. The failure happens during master initialization, before the ready to handle connections notice.
This is a control-plane failure, distinct from worker exhaustion or saturation cascades. Those affect a running master. Here, the usual FPM metrics (active processes, listen queue, max children reached) are unavailable because the status page and ping endpoint do not exist. The diagnostic path is different: read the first fatal log line.
PHP-FPM logs the bind address it tried, the file it failed to open, the extension that would not load, and the PID file it could not create. The error log plus journalctl -u php-fpm will almost always name the cause on the first or second line.
What this means
A PHP-FPM master process goes through a fixed startup sequence before forking any workers: parse the global config, load pool definitions, resolve include directives, open the error log, create the listening socket or sockets, and write the PID file. A failure at any step aborts startup before workers exist.
Workers never appear, so the FastCGI socket file is absent. Monitoring that worked yesterday returns connection refused.
flowchart TD
A["systemctl start: FAILED"] --> B{"php-fpm -t exits 0?"}
B -->|No| C["Config or extension error"]
B -->|Yes| D{"Fatal line mentions bind?"}
D -->|Yes| E{"Socket or port in use?"}
E -->|Stale holder| F["Kill stale process"]
E -->|Real conflict| G["Two pools or another daemon"]
D -->|No| H{"Error mentions PID file?"}
H -->|Yes| I["Check /run path and perms"]
H -->|No| J["Check AppArmor/SELinux"]The web server symptom is uniform: 502 Bad Gateway on every PHP request, with nginx or Apache logs showing connect() to ... failed or Connection refused. That uniformity does not help you find the cause. The FPM error log does.
Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Bind failure (address in use) | ERROR: unable to bind listening socket for address '...': Address already in use | ss -lxnp | grep php and ss -tlnp | grep 9000 |
| Two pools on same listen | ERROR: unable to set listen address as it's already used in another pool | grep -R '^listen' /etc/php/*/fpm/pool.d/ |
| Config parse error | ERROR: failed to open configuration file or ERROR: ... unknown entry | php-fpm -t and read the line number |
| Extension load failure | PHP Startup: Unable to load dynamic library '...' | php -m from CLI vs FPM SAPI |
| PID file path or permission | Unable to create the PID file (...): No such file or directory or Permission denied | ls -ld /run/php and pid = in php-fpm.conf |
| MAC denial (AppArmor/SELinux) | failed to open configuration file: Permission denied (13) with no obvious FS cause | aa-status, ausearch -m avc, journalctl -k | grep -i denied |
Bind failure is the most common, followed by config syntax errors introduced during deploy. PID file problems tend to appear after package upgrades that change defaults or after moving the runtime directory.
Quick checks
Run these before changing anything. They are all read-only.
# Daemon state and recent journal
systemctl status php-fpm --no-pager
journalctl -u php-fpm -n 80 --no-pager
# Config syntax test - exits 0 on success, non-zero with the offending file and line
php-fpm -t
# On distros that version the binary:
php-fpm8.3 -t
# Look for a stale holder of the listen socket or TCP port
ss -lxnp | grep -E 'php-fpm|www'
ss -tlnp | grep ':9000'
# List any php-fpm processes still alive (often a zombie master from a previous run)
pgrep -af php-fpm
# Read the first fatal line from the FPM error log
tail -n 50 /var/log/php-fpm.log 2>/dev/null || tail -n 50 /var/log/php8.3-fpm.log
If php-fpm -t already reports the failure, fix the file and line it names. If php-fpm -t exits 0 but systemctl start still fails, the problem is runtime: bind conflict, PID file, or a MAC denial that only triggers when systemd launches the binary under its label.
How to diagnose it
- Confirm the master is genuinely absent.
pgrep -af php-fpmshould return nothing, or only a defunct entry. If a master is running, you have a different problem, probably a stale socket file left behind by a previous crash. - Read the journal, not just the systemd status line.
systemctl statustruncates.journalctl -u php-fpm -n 80gives you the full startup attempt including the first fatal line. - Run
php-fpm -tas the same user and environment systemd uses. A config that tests clean as an interactive root shell can fail under the service’s MAC label or with a differentPHP_INI_SCAN_DIR. Ifsudo php-fpm -tpasses butsystemctl startfails, suspect a MAC denial rather than a syntax error. - Categorize the fatal line. Bind, config open, extension load, PID create, and permission denied (with no obvious filesystem cause) each point to a different fix branch below.
- For bind errors, distinguish stale holder from real conflict. A previous master that did not clean up its socket is the common sub-case. A second pool or another daemon on the same port is rarer but happens after config merges.
- For permission denied on the config file itself, check the MAC subsystem before file permissions. AppArmor and SELinux will deny reads even when
ls -lshows the file is world-readable.
Metrics and signals to monitor
Once the master is up, normal FPM metrics apply. During a startup failure, the signals that matter are the ones that tell you the master is absent and how long it has been absent.
| Signal | Why it matters | Warning sign |
|---|---|---|
Master process presence (pgrep -f 'php-fpm: master') | Binary “is the master alive” check | Absent for longer than the expected restart window |
Listen socket or port existence (ss -lxnp, ss -tlnp) | The web server cannot connect without it | Socket file missing or owned by a stale PID |
| Web server 5xx rate (502 specifically) | User-facing symptom | 100% of PHP requests returning 502 |
| systemd unit state | failed is a hard signal; activating loop indicates crash-on-start | Unit flapping between activating and failed |
| systemd restart count | Distinguishes a single failed start from a crash loop | Restart counter incrementing faster than humanly possible |
| FPM error log line rate | First fatal line per attempt is the diagnostic | Repeated identical fatal lines indicates deterministic config bug |
The standard FPM status page (pm.status_path) and ping endpoint (ping.path) are unavailable during this failure. The master never opens the listening socket, so there is nothing to query.
Fixes
Bind failure: stale holder vs real conflict
The canonical error is ERROR: unable to bind listening socket for address '127.0.0.1:9000': Address already in use or the Unix socket equivalent. Two root causes share this symptom.
Stale process. A previous master crashed without releasing the socket, or systemd killed the master but a child held the file descriptor. Identify the holder:
# For a TCP port
ss -tlnp | grep ':9000'
fuser -v 9000/tcp
# For a Unix socket
ss -lxnp | grep php-fpm.sock
lsof /run/php/php-fpm.sock
If the holder is a defunct or orphaned php-fpm process, kill it explicitly:
# Destructive - confirm the PID belongs to a stale FPM process before running
kill $(cat /run/php/php-fpm.pid 2>/dev/null)
# Or by port - sends SIGKILL by default
fuser -k 9000/tcp
# Or remove an orphaned socket file after confirming nothing holds it
rm /run/php/php-fpm.sock
Do not blindly kill -9 PHP processes. Confirm the PID belongs to a stale FPM master or worker, not a different service sharing the port.
Real conflict. Two pool definitions with the same listen line, or another daemon (a second PHP-FPM instance, a misconfigured php-fpm8.x alongside php-fpm) on the same port:
grep -R '^listen' /etc/php/*/fpm/pool.d/
If two pools share an address, the error is unable to set listen address as it's already used in another pool. Fix the duplicate in one of the pool files.
Config parse error
php-fpm -t is the fastest path. It prints the file and line number of the failure:
ERROR: [/etc/php/8.3/fpm/pool.d/www.conf:142] unknown entry 'pm.max_spare'
ERROR: failed to load configuration file '/etc/php/8.3/fpm/php-fpm.conf'
Common sub-causes:
- A
php_admin_value[]orphp_admin_flag[]line with a typo or missing value. - A pool directive typo, such as
pm.max_spareinstead ofpm.max_spare_servers. - An
includeglob that matches a file with bad permissions or a broken symlink. - An
env[...]entry with a space instead of an equals sign.
php-fpm -t runs the full parse including pool files. If it passes but systemctl start fails, the parse is fine and the failure is runtime.
Extension load failure
A missing or incompatible .so referenced in php.ini or via php_admin_value[extension] in a pool config can abort FPM startup before the master opens its socket:
PHP Warning: PHP Startup: Unable to load dynamic library 'redis.so' (tried: /usr/lib/php/20230831/redis.so)
Most PHP Startup warnings are non-fatal, but a hard failure during extension initialization (missing symbol, ABI mismatch, missing dependency .so) will terminate the master before it reaches the socket bind step.
CLI and FPM often use different INI scan directories. A module enabled for CLI may not be enabled for FPM, or vice versa. Compare:
php -m | sort > /tmp/cli-modules.txt
# Then check what FPM resolves with test-and-dump
php-fpm -tt 2>&1 | grep -i extension
The fix is to install the missing .so, comment out the offending extension= line, or rebuild the extension against the running PHP ABI.
PID file path or permission
Two related errors:
ERROR: Unable to create the PID file (/run/php/php-fpm.pid): No such file or directory (2)
ERROR: Unable to create the PID file (/run/php/php-fpm.pid): Permission denied (13)
The first means /run/php does not exist or was swept by a tmpfiles reset. The second means the directory exists but the FPM master user cannot write to it.
ls -ld /run /run/php
# Create the directory if missing, owned so the master can write
install -d -o root -g root -m 0755 /run/php
Check that pid = in php-fpm.conf points at a path inside a directory the master can write to. On systemd-managed systems, also check that the unit’s PIDFile= (if present) matches pid = exactly. A mismatch causes systemd to report the service as failed even when FPM is actually running.
On newer distros, the upstream unit runs with --nodaemonize, and PIDFile= is being phased out of upstream service files. If you maintain a custom unit with Type=forking, the PIDFile= path must match pid = byte-for-byte, or systemd will declare the start job failed.
MAC denial (AppArmor or SELinux)
If php-fpm -t as root passes, file permissions look correct, and the journal still shows failed to open configuration file: Permission denied (13), suspect a mandatory access control system. The denial happens at the kernel LSM layer, not the filesystem layer.
# AppArmor
aa-status
journalctl -k | grep -i 'apparmor.*DENIED\|php'
# SELinux
ausearch -m avc -ts recent | grep php
getenforce
The profile may need updating after a package upgrade that moved the config path, or after you added a new pool file outside the profile’s allowed read paths. This is common on Ubuntu and openSUSE after PHP major-version upgrades.
Prevention
- Run
php-fpm -tin CI and as a pre-deploy hook. It catches syntax errors, typos in pool directives, and brokenincludeglobs before they reach production. A non-zero exit blocks the deploy. - Align
pid =and systemdPIDFile=. If you maintain a custom unit withType=forking, treat the two as a single contract. Changing one without the other is a recurring source of failed starts after package upgrades. - Standardize socket ownership. Set
listen.owner,listen.group, andlisten.modeexplicitly in pool configs rather than relying on defaults. The commonlisten.modeof0660plus a mismatched web server user is a frequent silent failure after a package reinstall. - Use
RuntimeDirectory=phpin the systemd unit. This ensures/run/phpis created at boot and on restart, eliminating the “PID file directory missing” class of failures. - Pin extension versions and test the PHP ABI. Extension load failures usually follow a PHP minor upgrade. Run
php -mandphp-fpm -ttas part of the upgrade runbook.
How Netdata helps
During a startup failure the master is down, so the usual FPM status-page metrics are unavailable. Netdata’s value is detecting the absence fast and correlating it with adjacent signals.
- Process presence checks flag a missing
php-fpm: masterprocess within seconds, independent of the status page. - systemd unit state is collected directly, so a transition to
failedor a restart loop surfaces immediately alongside the journal. - Web server 5xx rate (nginx, Apache) correlates the FPM outage with user-facing 502s, confirming impact without manual log greps.
- Port and socket availability checks distinguish a true startup failure (socket absent) from a worker saturation event (socket present but workers stuck).
- Journal log collection surfaces the first fatal line in the same view as the metrics.
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






