# RFC: Single binary for CLI and FPM

- Status: Under discussion
- Authors: Matthieu Napoli, matthieu@mnapoli.fr
- Date: 2026-10-08
- Version: 0.2
- Implementation: https://github.com/php/php-src/pull/24192
- Original: https://wiki.php.net/rfc/single-binary-cli-fpm
- Page: https://externals.io/rfc/single-binary-cli-fpm
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

## Votes

### Link the CLI SAPI into php-fpm, so that it runs as the CLI when invoked as php, as outlined in the RFC? (closed)

Yes: 0 · No: 0 · Abstain: 0

## Discussions on the mailing list

- [RFC karma request](https://externals.io/message/132508.md) — started on 2026-09-15, 1 messages
- [\[RFC\] Single binary for CLI and FPM](https://externals.io/message/132806.md) — started on 2026-10-06, 12 messages
- [\[RFC\] Single binary for CLI and FPM](https://externals.io/message/132859.md) — started on 2026-10-10, 1 messages

---

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

```bash
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](https://bref.sh), 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:

```bash
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:

```bash
/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](https://github.com/php/php-src/pull/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](https://github.com/php/php-src/pull/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

- [Implementation, PR #24192](https://github.com/php/php-src/pull/24192)
- [Implementation of the first version, PR #23558](https://github.com/php/php-src/pull/23558)
- [Make php-cli functionality available in embed build, PR #21385](https://github.com/php/php-src/pull/21385)
- [Initial discussion on internals](https://news-web.php.net/php.internals/132508)
- [RFC discussion on internals](https://news-web.php.net/php.internals/132806)
- [PHP manual: FPM](https://www.php.net/manual/en/install.fpm.php)

## 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](#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.
