Table of Contents

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

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]

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:

Two differences are visible in a combined build:

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:

Internal changes, listed in UPGRADING.INTERNALS:

Proposed PHP version

PHP 8.7.

RFC impact

To SAPIs

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

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:

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

Future scope

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

Implementation

To be completed after the vote.

References

Rejected features

None yet.

Changelog