Take the State of PHP 2026 Survey →

Single binary for CLI and FPM View as Markdown

Under discussion
▴ 0 ▾ 0
community vote
Authors
Matthieu Napoli, matthieu@mnapoli.fr
Date
2026-10-08
Version
0.2
Implementation
https://github.com/php/php-src/pull/24192
Source
wiki.php.net/rfc/single-binary-cli-fpm

Vote

Link the CLI SAPI into php-fpm, so that it runs as the CLI when invoked as php, as outlined in the RFC? no votes
Yes · No · Abstain

Discussions on the mailing list

RFC karma request
by Marc Henderkes · 1 message
[RFC] Single binary for CLI and FPM
by Matthieu Napoli · 12 messages
[RFC] Single binary for CLI and FPM
by Marc Henderkes · 1 message

RFC text

Copied from wiki.php.net, last synchronized Oct 13, 2026 11:49 UTC

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 php and php-fpm executables;
  • the same build with a combined executable installed as php, and php-fpm replaced by a symlink to it. Bref itself was not modified: it starts php-fpm as 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_BINARY is /usr/bin/php, which runs the CLI, as today.
  • In FPM mode, PHP_BINARY is /usr/bin/php too, where it is /usr/sbin/php-fpm today. 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 call php-fpm keep 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 the php-fpm symlink.
  • Tools that look at the executable of a process (for example /proc/<pid>/exe) see php instead of php-fpm for 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-fpm invoked under a name such as php or php8.7 runs the CLI. Before, it ran FPM under any name.
  • The php-fpm executable is slightly larger (about 150 KB in a minimal build).
  • 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.

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 php executable is unchanged.
  • FPM: runs as the CLI when invoked under a php name, 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 php and php-fpm as separate executables, as today. The only difference is that php-fpm contains the CLI.
  • Ship a single executable, installed as 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, 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 php executable, which almost every PHP installation uses, for a feature only some need: it added a --fpm option to the CLI, and FPM's optional library dependencies (systemd, ACL, AppArmor, SELinux) to the php executable.
  • 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 install to install the single executable layout (php and a php-fpm symlink).
  • 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:

Vote: Link the CLI SAPI into php-fpm, so that it runs as the CLI when invoked as php, as outlined in the RFC? — Yes: 0 · No: 0 · Abstain: 0 (closed)

Patches and tests

  • Implementation: https://github.com/php/php-src/pull/24192
  • sapi/fpm/tests/main-cli.phpt checks that php-fpm runs as the CLI under the names php and php8, 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 php symlink to php-fpm.

Implementation

To be completed after the vote.

References

Rejected features

  • Linking FPM into the php executable, with a --enable-cli-fpm option and a php --fpm option (version 0.1). See Alternatives considered.

Changelog

  • 0.2 (2026-10-08): link the CLI into php-fpm instead of FPM into php, following the discussion. No configure option, no --fpm option, and a single vote.
  • 0.1 (2026-10-06): initial draft.