There are quite a few PHP runtimes right now and all of them address performance as one of the main concerns. So the obvious question is, which one is actually the fastest. Since runtimes are hard to compare in total, we have to start somewhere: I chose HTTP Server as the first use case to compare the runtimes in.
This whole benchmark is oriented on "Performance benchmark of PHP runtimes" by Dzmitry Kazbiarovich from January 2024, which lacks AMPHP and ReactPHP as runtime alternatives and is pretty focused on Symfony.
So this benchmarks work independent of Symfony. The actual measurements are performed by k6 by Grafana Labs.
- AMPHP
- FrankenPHP (classic and worker mode)
- OpenSwoole
- ReactPHP
- RoadRunner
- Swoole
If you want to see other alternatives, please let me know!
As references I chose Apache mod_php with mpm_prefork as baseline, Rust Rocket as some kind of upper limit and NodeJS as the probably main competitor.
AMPHP uses modern PHP features like Fibers to provide pseudo-parallel execution with Coroutines.
This benchmark currently does not look at event loop extensions, which are supported by Revolt (which is internally used by AMPHP). See https://revolt.run/extensions.
FrankenPHP and RoadRunner are different Go implementations of the PHP runtime.
ReactPHP is a PHP library for event-driven programming introducing an event loop.
Swoole and OpenSwoole are C++ extensions for PHP which include e.g. an HTTP server and provide Coroutine, Thread and Process based concepts. OpenSwoole is actually a fork of Swoole.
During the benchmark, the servers handled as many requests as they can in a fixed amount of time and with different amounts of concurrent requests.
The servers respond with a simple "Hello, world!"
and a Content-Type: text/plain
header as well as a status code 200.
The following numbers have been measured/calculated by k6:
Raw numbers
All HTTP servers run in an Alpine-based PHP 8.4 Docker image limited to a single CPU core to get comparable results. Memory is not limited since it is not expected to make any difference here.
Runtime | VUS | Requests per second | Average response time (ms) |
---|---|---|---|
Apache mod_php mpm_prefork | 10 | 6,612 | 1.46 |
Apache mod_php mpm_prefork | 100 | 5,819 | 17.1 |
Apache mod_php mpm_prefork | 1000 | 2,896 | 219 |
AMPHP (amphp/[email protected]) | 10 | 239 | 41.7 |
AMPHP (amphp/[email protected]) | 100 | 2,380 | 41.9 |
AMPHP (amphp/[email protected]) | 1000 | 14,482 | 68.8 |
FrankenPHP classic mode ([email protected]) | 10 | 9,260 | 1.05 |
FrankenPHP classic mode ([email protected]) | 100 | 8,948 | 11.1 |
FrankenPHP classic mode ([email protected]) | 1000 | 8,755 | 114 |
FrankenPHP worker mode ([email protected]) | 10 | 13,609 | 0.71 |
FrankenPHP worker mode ([email protected]) | 100 | 12,548 | 7.93 |
FrankenPHP worker mode ([email protected]) | 1000 | 12,578 | 79.2 |
OpenSwoole ([email protected], 1 reactor thread, 1 worker process) | 10 | 24,905 | 0.376 |
OpenSwoole ([email protected], 1 reactor thread, 1 worker process) | 100 | 24,352 | 4.07 |
OpenSwoole ([email protected], 1 reactor thread, 1 worker process) | 1000 | 22,385 | 44.5 |
OpenSwoole ([email protected], 1 reactor thread, 2 worker processes) | 10 | 21,317 | 0.439 |
OpenSwoole ([email protected], 1 reactor thread, 2 worker processes) | 100 | 20,094 | 4.69 |
OpenSwoole ([email protected], 1 reactor thread, 2 worker processes) | 1000 | 21,143 | 47.1 |
OpenSwoole ([email protected], 1 reactor thread, 3 worker processes) | 10 | 19,098 | 0.49 |
OpenSwoole ([email protected], 1 reactor thread, 3 worker processes) | 100 | 20,134 | 4.91 |
OpenSwoole ([email protected], 1 reactor thread, 3 worker processes) | 1000 | 20,828 | 47.9 |
OpenSwoole ([email protected], 2 reactor threads, 2 worker processes) | 10 | 18,450 | 0.509 |
OpenSwoole ([email protected], 2 reactor threads, 2 worker processes) | 100 | 18,114 | 5.47 |
OpenSwoole ([email protected], 2 reactor threads, 2 worker processes) | 1000 | 17,202 | 57.9 |
ReactPHP (react/[email protected]) | 10 | 39,928 | 0.188 |
ReactPHP (react/[email protected]) | 100 | 40,718 | 2.41 |
ReactPHP (react/[email protected]) | 1000 | 35,302 | 28.2 |
RoadRunner ([email protected], http.pool.num_workers=1) | 10 | 7,111 | 1.37 |
RoadRunner ([email protected], http.pool.num_workers=1) | 100 | 6,857 | 14.5 |
RoadRunner ([email protected], http.pool.num_workers=1) | 1000 | 6,860 | 145 |
RoadRunner ([email protected], http.pool.num_workers=2) | 10 | 7,299 | 1.34 |
RoadRunner ([email protected], http.pool.num_workers=2) | 100 | 7,063 | 14.1 |
RoadRunner ([email protected], http.pool.num_workers=2) | 1000 | 6,968 | 143 |
RoadRunner ([email protected], http.pool.num_workers=3) | 10 | 6,463 | 1.51 |
RoadRunner ([email protected], http.pool.num_workers=3) | 100 | 6,686 | 14.9 |
RoadRunner ([email protected], http.pool.num_workers=3) | 1000 | 6,029 | 165 |
Swoole ([email protected]) | 10 | 44,256 | 0.202 |
Swoole ([email protected]) | 100 | 44,800 | 2.2 |
Swoole ([email protected]) | 1000 | 40,110 | 24.8 |
Rust Rocket (v0.5.1, workers=1) | 10 | 52,070 | 0.168 |
Rust Rocket (v0.5.1, workers=1) | 100 | 50,897 | 1.93 |
Rust Rocket (v0.5.1, workers=1) | 1000 | 45,434 | 21.9 |
NodeJS (v23.11.0) | 10 | 53,421 | 0.163 |
NodeJS (v23.11.0) | 100 | 47,538 | 2.07 |
NodeJS (v23.11.0) | 1000 | 41,241 | 24.2 |
First of all, it should be noted, that I am comparing mostly stock configurations here. There are most probably ways to tweak the performance of the individual runtimes. Feel free to look at the server implementations and test your own configurations (and please let me know if you find something interesting!).
- The average response time seems to be roughly proportional to the amount of concurrent requests.
- AMPHP is really bad below 1000 parallel requests, but it outperformes FrankenPHP and RoadRunner at 1000 parallel requests.
- Although ReactPHP is plain PHP - no Go, no C++ - it is way faster than I expected.
- Why is OpenSwoole about half as fast as Swoole? They are expected to be quite similar.
- Start a server of your choice from the
src
folder.cd src/<runtime>
docker build -t cracksalad/php-runtime-benchmark-http-server-<runtime> .
docker run --rm --cpus 1 -p 1337:1337 -it cracksalad/php-runtime-benchmark-http-server-<runtime>
- Run
k6 run --vus <VUS> bench/mark.ts
with<VUS>
being the number of parallel executions. - Wait 30 seconds and voilà!