Skip to content
← All packages

phpfpm

A Go library for talking to PHP-FPM: discover running masters, parse their effective configuration, scrape live status over FastCGI, and reload a master safely. The shared layer beneath fpm-exporter and fpm-tune.

Two of our tools need the same things from PHP-FPM: find the masters running on a host, work out what they are actually configured to do, read what their pools are doing right now, and in one case change it. fpm-exporter reads a host, fpm-tune reads and writes one. Neither depends on the other, and both depend on this.

Validate, then reload

SIGUSR2 makes a master re-read its configuration and cycle workers gracefully, carrying its listening sockets across so no request is dropped. But a configuration php-fpm rejects does not degrade: the master refuses to come back, and every pool it served goes with it. Anything changing pm.* must call Validate first. A drop-in naming a pool that no longer exists makes php-fpm -t exit 78, and reloading with that file present kills the master permanently.

The parts that refuse to do the obvious thing

The interesting code here is the code that declines the easy implementation, because the easy one is wrong in a way that only surfaces in production. Each of these was found against a real master rather than reasoned about.

Found The Hard Way

A Reload Does Not Preserve The PID

SIGUSR2 makes the master re-exec itself. In the foreground the pid survives; daemonized, which is php-fpm's own default, the re-exec produces a new process and the original exits. A consumer that watched the pid it signalled reported a textbook reload as a dead master and rolled a good change back.

Identity Before Signalling

A pid is a promise about the instant it was read. A master can exit between discovery and the reload and the kernel can reuse the number, so the reload compares process start time across the settle window, and matches the config path as a path rather than a substring so a host running two masters signals the right one.

The Process Table Is Not A Trust Boundary

Any local user can start a process named like php-fpm whose command line names a config they control, and both get handed to exec. Discovery checks ownership, refuses relative paths, and as root checks the directories above them, since a root-owned binary in a world-writable directory can be swapped between the check and the exec.

Finding A Master Without Reading Its Config

Discover parses each master's config to enumerate pools, so a master whose configuration no longer parses is invisible to it, which is exactly when something trying to repair that configuration most needs to find it. DiscoverMasters reads only the process table and answers anyway.

The Config Cache Never Expires

Parsing forks php-fpm, so the result is cached for the life of the process. Right for a scrape loop, wrong for anything that changes the configuration. A consumer that missed the invalidation call reported a pool as configured for 4 workers hours after setting it to 12.

An Unscrapable Pool Still Costs Memory

A failed scrape still carries the pool's configured max_children and process manager. Without that, a site restarting for five seconds looks like a pool needing nothing, and its memory gets handed to its neighbours.

Where it runs, and as whom

It shells out to the php-fpm binary to parse and validate, scans /proc to find masters, and signals them, so it runs where php-fpm runs, which in practice means Linux. Elsewhere the process scan and the trust checks degrade rather than pretend. The user matters too: as root it reads everything and applies the strict ownership checks, as the php-fpm user it reads its own master, and as a stranger it sees less, on purpose.

There is no package-level logger. Every entry point that can log takes a logger, because a library owning a global one cannot be embedded twice with different destinations and makes tests order-dependent.