====== PHP RFC: Single binary for CLI and FPM ====== * Version: 0.1 * Date: 2026-10-06 * Author: Matthieu Napoli, matthieu@mnapoli.fr * Status: Under discussion * Implementation: https://github.com/php/php-src/pull/23558 * Discussion thread: https://news-web.php.net/php.internals/132806 * Voting thread: tbd ===== Introduction ===== 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. ===== Motivation ===== 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 [[https://bref.sh|Bref]], which runs PHP on AWS Lambda, by comparing two builds of its PHP runtime (called "layer" in Lambda): * the current layer, with separate ''php'' and ''php-fpm'' executables; * the same build with ''--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. ==== Smaller distributions ==== ^ ^ 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. ==== Faster FPM start after a CLI command ==== 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] | * The whole gain is in Lambda's initialization phase, which is when FPM starts. Warm requests are not affected. The maximum memory used reported by Lambda drops by 9 MB. * The last row is a control. That runtime never starts FPM, so it sees no difference, although its layer is just as much smaller. On Lambda, the smaller package doesn't speed up cold starts by itself: the gain comes from not loading the same code twice. * ARM and other memory sizes were not measured. 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. ===== Proposal ===== ==== Build option ==== 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|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''. ==== Running FPM ==== A combined ''php'' executable runs FPM in two cases: - **Its first argument is ''--fpm''.** The remaining arguments are FPM's usual options. - **It is invoked under a name that starts with ''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 behaviour ==== 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: * Process titles are unchanged: ''php-fpm: master process (/etc/php-fpm.conf)'' and ''php-fpm: pool www''. * Graceful reload (''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. * The whole FPM test suite (''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. * Tools that look at the executable of a process (for example ''/proc//exe'') see ''php'' instead of ''php-fpm''. ==== Changes to the standalone FPM executable ==== 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 [[https://github.com/php/php-src/pull/21385|CLI/embed change in PHP 8.6]], which made ''do_php_cli()'' available to the embed SAPI. ===== Backward incompatible changes ===== 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. * A CLI executable installed or linked under a name starting with ''php-fpm'' starts FPM. * The CLI executable is slightly larger (138 KB, or 0.6%, in the build measured above), and it links the optional libraries FPM uses when they are enabled at build time (systemd, ACL, AppArmor, SELinux). Internal changes, listed in UPGRADING.INTERNALS: * The FPM entry point is now ''fpm_main()''. * ''--enable-fpm'' is declared in ''sapi/fpm/config0.m4'', so that other SAPIs can check ''$PHP_FPM''. * The FPM sources other than ''main()'' are collected in ''PHP_FPM_SHARED_OBJS'', like ''PHP_CLI_SHARED_OBJS'' for the CLI. * FPM's C implementation of ''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. ===== Proposed PHP version ===== PHP 8.7. ===== RFC impact ===== ==== To SAPIs ==== * CLI: in a combined build, gains the two ways of starting FPM described above. Nothing changes otherwise. * FPM: no behavior change. * Embed and other SAPIs: not affected. * Windows: not affected (FPM is not available on Windows). ==== To existing extensions ==== None. Extensions are the same in both modes, and extensions that check the SAPI name keep seeing ''cli'' or ''fpm-fcgi''. ==== To packaging ==== The option gives distributors a choice (it doesn't force one): * Keep shipping ''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. * Ship only ''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. ===== Alternatives considered ===== ==== A shared library ==== 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: * It is a much larger change to the Unix build system and to how distributions package PHP. * On Unix, PHP links the engine statically into each executable. Moving it into a shared library means compiling it as position-independent code for a library and adds dynamic linking work to every process start. Startup time is precisely what matters in the motivating use case. 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. ==== Selecting FPM only by executable name ==== 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. ==== Removing --fpm from the arguments passed to FPM ==== 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''. ===== Open issues ===== * Feedback from packagers would be welcome. ===== Future scope ===== * Making FPM available through the embed SAPI (''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. * Installing ''php-fpm'' as a symlink to ''php'' by default in ''make install''. * The shared library approach described above. ===== Voting choices ===== Primary vote, requiring a 2/3 majority: * Yes * No * Abstain 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. * Yes, enabled by default (--disable-cli-fpm to opt out) * No, opt-in * Abstain ===== Patches and tests ===== * Implementation: https://github.com/php/php-src/pull/23558 * ''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. * The FPM test suite also passes when the test harness runs FPM through a combined executable, either as ''php --fpm'' or through a ''php-fpm'' symlink to ''php''. ===== Implementation ===== To be completed after the vote. ===== References ===== * [[https://github.com/php/php-src/pull/23558|Implementation, PR #23558]] * [[https://github.com/php/php-src/pull/21385|Make php-cli functionality available in embed build, PR #21385]] * [[https://news-web.php.net/php.internals/132508|Initial discussion on internals]] * [[https://www.php.net/manual/en/install.fpm.php|PHP manual: FPM]] ===== Rejected features ===== None yet. ===== Changelog ===== * 0.1 (2026-10-06): initial draft.