Modern PHP web servers
Moderní PHP webservery. Apache, Nginx, Caddy, NGINX Unit and FrankenPHP. They all serve PHP, but they do it in very different ways, and that difference matters more than any benchmark.
First, how PHP gets served
PHP is an interpreted language. The interpreter, php-src, is written in C, and a web server can’t run PHP by itself. It needs a SAPI, a Server Application Programming Interface, which is the bridge between the web server and the interpreter.
There are two ways to do it:
- External: the web server talks to a separate PHP-FPM process over the FastCGI protocol. PHP-FPM has a process manager that keeps a pool of PHP workers ready.
- Embedded: the interpreter lives inside the web server process. Apache with mod_php, Unit and FrankenPHP work this way.
This matters for containers. An external SAPI means two processes, usually two containers, that you have to wire together. An embedded one is a single process and a single container.
What I expect from a web server
- Stability and performance
- Easy to configure and run
- Current PHP (8.3 at the time of the talk)
- Open source, good documentation and a community
- Cloud friendly, with observability such as Prometheus metrics
Apache (since 1995)
Twenty-nine years old at the time of the talk and still everywhere. It runs PHP either embedded through mod_php, which fits in a single container, or through PHP-FPM. It’s written in C, it’s stable, and it has the legendary .htaccess. On the other side: performance isn’t great, and configuring it is painful.
Nginx (since 2004)
The industry standard. Written in C, with great performance and an excellent reverse proxy. It only talks to PHP through PHP-FPM, which makes it less container friendly: you always run two processes. It’s also complex to configure well.
Caddy (since 2015)
Written in Go, and the easiest one to start with. Its defaults are good enough for production as they are:
- Automatic HTTPS out of the box
- HTTP/2 and HTTP/3 without extra configuration
- Zero-downtime config reloads, through the Caddyfile or a REST API
- A good reverse proxy, modular, with a big community (54k GitHub stars at the time)
The catch: it uses PHP-FPM like Nginx, and it’s slower than Nginx.
NGINX Unit (since 2017)
Unit is a different idea: an application server that runs many languages (PHP, Python, Node, Go, Java, Perl, Ruby, WebAssembly) and embeds PHP directly. You configure it entirely through a REST API with an OpenAPI spec, and routing goes through listeners, routes and applications or upstreams.
The good parts were great performance and stability with a small footprint, zero-downtime reloads, powerful routing with NJS scripting, easy TLS, and security. The weak parts were no native Prometheus support, a slower release cycle than I hoped, and some missing advanced features.
FrankenPHP (since 2022)
FrankenPHP is by Kévin Dunglas, the author of Mercure, Vulcain and API Platform. It’s a combination of three things:
- Caddy as the web server,
- FrankenPHP as a SAPI embedded into Caddy, written in Go with
//go:embed, - optionally static-php-cli, which compiles PHP statically, so the whole app can ship as one self-contained binary.
You get everything Caddy offers (HTTP/2 and HTTP/3, automatic HTTPS, zero-downtime reloads), plus:
- Worker mode: the app boots once and stays in memory between requests. This is where the speed comes from.
- A built-in Mercure hub for real-time apps
- 103 Early Hints
- Convention over configuration
- Native support for Symfony, Laravel, API Platform, Drupal and WordPress
My worries in 2024 were stability and a bus factor of one.
The benchmarks
Numbers from a benchmark of PHP runtimes, in a Docker container with 1 CPU and 1 GB of RAM, measured with k6 at a concurrency of 1,000:
FrankenPHP in worker mode handled about 5,470 requests per second, 25× more than Apache and more than 12× Nginx with PHP-FPM. Its average response time was 180 ms, against almost 3 seconds for Apache. Without worker mode, FrankenPHP was roughly on par with Nginx and PHP-FPM. Unit was the best of the classic setups at about 1,380 requests per second.
A separate stress test of Unit, Nginx with PHP-FPM, and Laravel Octane on Swoole found that Unit was the only one that didn’t drop a single request, with low resource usage and very fast cold starts.
Which one to pick
- Nginx over Apache: more performant, better at load balancing and reverse proxying. Apache uses more resources because of its process-driven architecture.
- Caddy vs Nginx: Caddy is far easier to start with, so it’s the go-to choice for beginners and small sites. Nginx is faster, it’s the industry standard, and it has the bigger ecosystem.
- Unit: my 2024 verdict was “very fast and reliable, go for it”. It’s archived now, so no longer.
- FrankenPHP: super cool, go for it. With the PHP Foundation behind it, that’s an even easier call today.
The demo code from the talk is on GitHub: github.com/janmikes/phplive-2024.








































