Today, the php and php-fpm executables contain the same engine and the same extensions. Only a small part of the code (the FPM SAPI and its FastCGI layer) differs. Distributions that need both ship that code twice, and when FPM starts right after a CLI command, it loads that code a second time from a different file.
This RFC proposes a build option, --enable-cli-fpm, that links the FPM SAPI into the php executable. One binary can then run both the CLI and FPM:
php script.php # CLI, as today php --fpm --nodaemonize -y /etc/php-fpm.conf # FPM ln -s php php-fpm ./php-fpm --nodaemonize -y /etc/php-fpm.conf # FPM, through a symlink
With a single binary, all the PHP and PHP-FPM code is shipped once and loaded once.
A single executable brings two separate benefits: smaller PHP distributions, and a faster FPM start when FPM follows a CLI command. Both were measured with Bref, which runs PHP on AWS Lambda, by comparing two builds of its PHP runtime (called “layer” in Lambda):
php and php-fpm executables;--enable-cli-fpm, and php-fpm replaced by a symlink to php. Bref itself was not modified: it starts php-fpm as before.Setup: the implementation backported to PHP 8.5.11, Linux x86-64, Bref 3.0.12, October 2026.
| Two executables | Single executable | |
|---|---|---|
php | 25,985,096 bytes | 25,986,576 bytes |
php-fpm | 25,985,744 bytes | symlink |
| Layer, zipped | 24.3 MB | 19.9 MB (-18%) |
| Layer, unzipped | 89.9 MB | 64.5 MB (-28%) |
The combined executable is barely larger than the CLI-only one: its code and data grow by 138 KB (+0.6%), which fits in the padding the linker adds to align segments. So the second executable's 26 MB are saved almost entirely.
This matters wherever space is limited and both SAPIs are needed. For example, Lambda caps a function and its layers at 250 MB unzipped, and container images, small devices and self-contained (statically linked) PHP distributions all ship both executables today.
On every cold start, Bref's runtime for web applications runs PHP twice: the CLI executes Bref's bootstrap script, which then starts FPM to serve requests. With two executables, FPM's start has to load its own copy of the engine, even though the bootstrap has just loaded the same code from php. With a single executable, php-fpm is the same file, already loaded.
Cold start durations, median of 50 cold starts per function, 1024 MB of memory, eu-west-3 region, with the 95% confidence interval of the difference:
| Application | Two executables | Single executable | Difference |
|---|---|---|---|
| Minimal application, FPM | 280 ms | 211 ms | -70 ms (-25%) [-79, -59] |
| Minimal application, without FPM | 195 ms | 193 ms | no difference [-6, +6] |
Starting faster is critical on Lambda, but it helps any platform that starts PHP on demand to absorb load: autoscaled containers, scale-to-zero services, serverless platforms. The same pattern happens there whenever a CLI command runs right before FPM starts, for example a container entry point that warms a cache or runs migrations. How much it saves depends on how costly it is to read the executable: some platforms load container images lazily like Lambda does, while on a host where the image is already on a local disk, the gain will be smaller. These measurements only cover Lambda.
A new configure option, --enable-cli-fpm, links the FPM SAPI into the CLI executable. It requires both the CLI and FPM SAPIs:
./configure --enable-cli --enable-fpm --enable-cli-fpm
| Configuration | Result |
|---|---|
CLI and FPM enabled, without --enable-cli-fpm | Separate php and php-fpm executables, as today |
CLI and FPM enabled, with --enable-cli-fpm | A php executable that can also run FPM, plus the standalone php-fpm executable |
--enable-cli-fpm without CLI or FPM | Configure error |
Whether the option is enabled by default when both SAPIs are built is decided by a secondary vote (see Voting choices). FPM itself stays disabled by default, so builds that don't enable FPM are not affected either way.
The standalone php-fpm executable is still built and installed. Distributors who want a single executable can install php and either omit php-fpm or replace it with a symlink to php.
A combined php executable runs FPM in two cases:
--fpm. The remaining arguments are FPM's usual options.php-fpm, for example through a php-fpm or php-fpm8.7 symlink (or a hard link, or a copy). All arguments are FPM's usual options.In every other case it is the CLI, unchanged:
php -v # PHP 8.7.0 (cli) php --fpm -v # PHP 8.7.0 (fpm-fcgi) php --fpm -t -y /etc/php-fpm.conf # test the FPM configuration php --fpm --nodaemonize -y /etc/php-fpm.conf # run FPM in the foreground php script.php --fpm # --fpm is passed to the script php -n --fpm # CLI error, as today: --fpm is only recognized first php-fpm8.7 --nodaemonize # FPM, if php-fpm8.7 is a symlink to php
The symlink form makes a combined binary a drop-in replacement for an existing php-fpm executable: service definitions, container entry points and scripts that call php-fpm keep working. The --fpm form works without creating a link and is listed in php --help. Matching names that start with php-fpm covers the names used by distributions (for example php-fpm8.3 on Debian or php-fpm83 on Alpine) and those produced by --program-suffix.
FPM runs exactly as the standalone executable does: same options, configuration files, process manager, signals and logs. PHP_SAPI is fpm-fcgi in FPM mode and cli otherwise. There is no way to switch from one SAPI to the other in a running process.
The following were verified with the implementation:
php-fpm: master process (/etc/php-fpm.conf) and php-fpm: pool www.SIGUSR2) works. FPM re-executes its original command line, so a master started as php --fpm … restarts as php --fpm …, and one started through a php-fpm symlink restarts through it.sapi/fpm/tests) passes when FPM is started through php --fpm and through a php-fpm symlink to php.Two differences are visible in a combined build:
PHP_BINARY in FPM mode is the path of the combined php executable, where it is the path of php-fpm today (a symlink is resolved to its target). Unlike php-fpm, that executable can also run CLI scripts./proc/<pid>/exe) see php instead of php-fpm.
The standalone php-fpm executable keeps its behavior. Internally, its main() becomes a call to a new fpm_main() function, which the CLI calls too. This follows the same approach as the CLI/embed change in PHP 8.6, which made do_php_cli() available to the embed SAPI.
None for builds that don't use the option.
In a combined build:
php --fpm starts FPM. Before, it was rejected as an unknown option.php-fpm starts FPM.Internal changes, listed in UPGRADING.INTERNALS:
fpm_main().--enable-fpm is declared in sapi/fpm/config0.m4, so that other SAPIs can check $PHP_FPM.main() are collected in PHP_FPM_SHARED_OBJS, like PHP_CLI_SHARED_OBJS for the CLI.apache_request_headers() is renamed from zif_apache_request_headers to zif_fpm_request_headers, because the CLI defines a function with the same C name. The PHP functions apache_request_headers() and getallheaders() are unchanged.PHP 8.7.
None. Extensions are the same in both modes, and extensions that check the SAPI name keep seeing cli or fpm-fcgi.
The option gives distributors a choice (it doesn't force one):
php and php-fpm as separate executables, as today. If the option is enabled by default, the only difference is that php can also run FPM.php, with php-fpm as a symlink to it. An FPM package would then contain the symlink, configuration and service files, and depend on the package that provides php.A combined executable uses one set of build options and extensions for both modes. Distributions that build the CLI and FPM with different configure options, in separate builds, are not affected: the option only applies when both SAPIs are built by the same configure run.
PHP could build the engine as a shared library (libphp.so) and make php and php-fpm small executables linked to it, as PHP does on Windows with php8.dll. Both executables would keep their names, and the shared code would be on disk once.
This RFC doesn't follow that approach:
A shared library would also avoid loading the engine twice, since both executables would map the same file. The two approaches are not exclusive. Moving FPM's entry point into fpm_main() is also a step towards a shared library that would contain both SAPIs.
Dispatching only on the executable name (no --fpm) would keep CLI argument handling untouched. But it would require a link to use FPM, and the feature would not be discoverable in php --help. The two mechanisms cost a few lines and serve different needs so the RFC proposes both.
FPM needs the process's original arguments: graceful reload re-executes them, and process titles are written over their memory. The CLI therefore passes them unchanged and tells FPM to start reading its options after --fpm.
libphp), the same way the CLI is available there since PHP 8.6. There is no concrete use case for it yet. The refactoring in this RFC makes it straightforward if one appears.php-fpm as a symlink to php by default in make install.Primary vote, requiring a 2/3 majority:
Secondary vote, decided by simple majority. Its result is void if the primary vote fails. In case of a tie, the option stays opt-in.
sapi/cli/tests/cli_fpm.phpt covers both ways of starting FPM and checks that --fpm is an ordinary argument in other positions. It is skipped when PHP is built without the option.php --fpm or through a php-fpm symlink to php.To be completed after the vote.
None yet.