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 Bref, which runs PHP on AWS Lambda, by comparing two builds of its PHP runtime (called “layer” in Lambda):
- the current layer, with separate
phpandphp-fpmexecutables; - the same build with
--enable-cli-fpm, andphp-fpmreplaced by a symlink tophp. Bref itself was not modified: it startsphp-fpmas 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). 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 aphp-fpmorphp-fpm8.7symlink (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)andphp-fpm: pool www. - Graceful reload (
SIGUSR2) works. FPM re-executes its original command line, so a master started asphp --fpm …restarts asphp --fpm …, and one started through aphp-fpmsymlink restarts through it. - The whole FPM test suite (
sapi/fpm/tests) passes when FPM is started throughphp --fpmand through aphp-fpmsymlink tophp.
Two differences are visible in a combined build:
PHP_BINARYin FPM mode is the path of the combinedphpexecutable, where it is the path ofphp-fpmtoday (a symlink is resolved to its target). Unlikephp-fpm, that executable can also run CLI scripts.- Tools that look at the executable of a process (for example
/proc/<pid>/exe) seephpinstead ofphp-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 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 --fpmstarts FPM. Before, it was rejected as an unknown option.- A CLI executable installed or linked under a name starting with
php-fpmstarts 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-fpmis declared insapi/fpm/config0.m4, so that other SAPIs can check$PHP_FPM.- The FPM sources other than
main()are collected inPHP_FPM_SHARED_OBJS, likePHP_CLI_SHARED_OBJSfor the CLI. - FPM's C implementation of
apache_request_headers()is renamed fromzif_apache_request_headerstozif_fpm_request_headers, because the CLI defines a function with the same C name. The PHP functionsapache_request_headers()andgetallheaders()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
phpandphp-fpmas separate executables, as today. If the option is enabled by default, the only difference is thatphpcan also run FPM. - Ship only
php, withphp-fpmas a symlink to it. An FPM package would then contain the symlink, configuration and service files, and depend on the package that providesphp.
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-fpmas a symlink tophpby default inmake install. - The shared library approach described above.
Voting choices
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.
Patches and tests
- Implementation: https://github.com/php/php-src/pull/23558
sapi/cli/tests/cli_fpm.phptcovers both ways of starting FPM and checks that--fpmis 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 --fpmor through aphp-fpmsymlink tophp.
Implementation
To be completed after the vote.
References
Rejected features
None yet.
Changelog
- 0.1 (2026-10-06): initial draft.