rfc:single-binary-cli-fpm

PHP RFC: Single binary for CLI and FPM

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 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). 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:

  1. Its first argument is --fpm. The remaining arguments are FPM's usual options.
  2. 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/<pid>/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 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:

Add the --enable-cli-fpm build option as outlined in the RFC?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

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.

Should --enable-cli-fpm be enabled by default when both the CLI and FPM SAPIs are built?
Real name Yes, enabled by default (--disable-cli-fpm to opt out) No, opt-in Abstain
Final result: 0 0 0
This poll has been closed.

Patches and tests

  • 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

Rejected features

None yet.

Changelog

  • 0.1 (2026-10-06): initial draft.
rfc/single-binary-cli-fpm.txt · Last modified: by mnapoli