PHP RFC: Single binary for CLI and FPM
- Version: 0.2
- Date: 2026-10-08
- Author: Matthieu Napoli, matthieu@mnapoli.fr
- Status: Under discussion
- 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 CLI and FPM SAPIs themselves) 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 to link the CLI SAPI into the php-fpm executable, and to make php-fpm run as the CLI when it is invoked under the name php. One binary can then run both the CLI and FPM:
php-fpm --nodaemonize -y /etc/php-fpm.conf # FPM, as today ln -s php-fpm php ./php script.php # CLI
The php executable is not changed. Distributions that keep shipping two executables are not affected. Those that want a single one can install the php-fpm executable as php, with php-fpm as a symlink to it, so that all the PHP 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 a combined executable installed as
php, andphp-fpmreplaced by a symlink to it. Bref itself was not modified: it startsphp-fpmas before.
Setup: 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 either separate one: the second SAPI adds about 140 KB of code and data (+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
When both the CLI and FPM SAPIs are built, the CLI SAPI is also linked into the php-fpm executable. There is no new configure option:
| Configuration | Result |
|---|---|
| CLI and FPM enabled | A php executable, unchanged, and a php-fpm executable that can also run the CLI |
FPM enabled, CLI disabled (--disable-cli) | A php-fpm executable, unchanged |
| FPM disabled (the default) | Not affected |
An option would only be useful if linking the CLI into php-fpm had a cost. It has none that we found: the CLI has no library dependencies of its own, so php-fpm only grows by the CLI code (about 150 KB in a minimal build), and its behavior under its own name is unchanged.
make install installs php and php-fpm as two separate executables, as today.
Running the CLI
php-fpm runs as the CLI when it is invoked under the name php, optionally followed by a digit, for example through a symlink (or a hard link, or a copy). All arguments are the CLI's usual arguments. Under any other name, it is FPM, unchanged:
php-fpm -v # PHP 8.7.0 (fpm-fcgi) php-fpm8.7 --nodaemonize # FPM ln -s php-fpm php php -v # PHP 8.7.0 (cli) php -S localhost:8000 # the built-in web server ln -s php-fpm php8.7 php8.7 script.php # CLI
The rule covers the names used by distributions for the CLI (for example php8.3 on Debian or php83 on Alpine), while leaving out other programs whose name starts with php: php-cgi, php-config, php-fpm, phpdbg, phpize. A name such as php-8.7 (from --program-suffix=-8.7) is not recognized, because it can't be told apart from those programs.
In CLI mode, the executable behaves exactly like the php executable: PHP_SAPI is cli, it reads the same php-cli.ini and accepts the same options. The whole CLI test suite (sapi/cli/tests) passes when run through a php symlink to php-fpm. There is no way to switch from one SAPI to the other in a running process.
Shipping a single executable
PHP_BINARY is the real path of the executable: symlinks are resolved. Scripts that run PHP_BINARY to start another PHP process, such as Composer restarting itself or PHPUnit's process isolation, expect it to run the CLI. A distribution that ships a single executable should therefore install it as php, with php-fpm as a symlink to it:
/usr/bin/php # the php-fpm executable /usr/sbin/php-fpm # symlink to /usr/bin/php
With this layout:
- In CLI mode,
PHP_BINARYis/usr/bin/php, which runs the CLI, as today. - In FPM mode,
PHP_BINARYis/usr/bin/phptoo, where it is/usr/sbin/php-fpmtoday. Running it now starts the CLI, which is more useful than starting FPM. - FPM is still started as
php-fpm, so service definitions, container entry points and scripts that callphp-fpmkeep working. Process titles (php-fpm: master process (/etc/php-fpm.conf),php-fpm: pool www) are unchanged, and graceful reload (SIGUSR2) works: FPM re-executes its original command line, through thephp-fpmsymlink. - Tools that look at the executable of a process (for example
/proc/<pid>/exe) seephpinstead ofphp-fpmfor FPM processes.
The opposite layout, php as a symlink to php-fpm, works for the CLI itself, but PHP_BINARY is then php-fpm, and scripts that run it would start FPM. This is documented in UPGRADING.
Backward incompatible changes
None for builds that don't include both SAPIs.
When both SAPIs are built:
php-fpminvoked under a name such asphporphp8.7runs the CLI. Before, it ran FPM under any name.- The
php-fpmexecutable is slightly larger (about 150 KB in a minimal build). - 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.
Internal changes, listed in UPGRADING.INTERNALS: when the CLI is built, PHP_FPM_OBJS includes PHP_CLI_SHARED_OBJS, and PHP_FPM_WITH_CLI is defined.
Proposed PHP version
PHP 8.7.
RFC impact
To SAPIs
- CLI: not affected. The
phpexecutable is unchanged. - FPM: runs as the CLI when invoked under a
phpname, as described above. Nothing changes otherwise. - 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
Nothing changes unless distributors choose to:
- Keep shipping
phpandphp-fpmas separate executables, as today. The only difference is thatphp-fpmcontains the CLI. - Ship a single executable, installed as
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, can keep doing so.
Alternatives considered
Linking FPM into the CLI
The first version of this RFC proposed the opposite: a --enable-cli-fpm configure option that linked FPM into the php executable, which then ran FPM with php --fpm or when invoked under a php-fpm name (PR #23558). It was changed following the discussion on internals:
- It changed the
phpexecutable, which almost every PHP installation uses, for a feature only some need: it added a--fpmoption to the CLI, and FPM's optional library dependencies (systemd, ACL, AppArmor, SELinux) to thephpexecutable. - It gave FPM a special place in the CLI, where other SAPIs could not get the same treatment. Linking the CLI into another SAPI is the approach PHP 8.6 already took for the embed SAPI (PR #21385), which FrankenPHP uses to ship a single executable. Any SAPI can do the same.
Selecting the CLI with an option
php-fpm could run the CLI with an option, for example php-fpm --cli script.php. It is not proposed: FPM's argument parsing stays untouched (some FPM and CLI options use the same letters, such as -F), the CLI receives its arguments unchanged, and a single executable needs to be installed as php for PHP_BINARY anyway.
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.
Open issues
None.
Future scope
- An option for
make installto install the single executable layout (phpand aphp-fpmsymlink). - Linking the CLI into other SAPIs that have their own executable, the same way.
- The shared library approach described above.
Voting choices
Primary vote, requiring a 2/3 majority:
Patches and tests
sapi/fpm/tests/main-cli.phptchecks thatphp-fpmruns as the CLI under the namesphpandphp8, and as FPM under other names. It is skipped when PHP is built without the CLI.- The CLI test suite also passes when run through a
phpsymlink tophp-fpm.
Implementation
To be completed after the vote.
References
Rejected features
- Linking FPM into the
phpexecutable, with a--enable-cli-fpmoption and aphp --fpmoption (version 0.1). See Alternatives considered.
Changelog
- 0.2 (2026-10-08): link the CLI into
php-fpminstead of FPM intophp, following the discussion. No configure option, no--fpmoption, and a single vote. - 0.1 (2026-10-06): initial draft.