Benchmark Report
Generated 2026-10-13 09:08:56 UTC. This report combines wrkx stage summaries with Docker CPU and memory samples.
Test system. OS: Ubuntu 26.04.1 LTS (x86_64); Kernel: Linux 7.0.0-34-generic; CPU: AMD Ryzen 9 7950X, 16 cores / 32 threads; Memory: 93 GiB usable RAM.
Run dates. 2026-10-05: FrankenPHP classic, FrankenPHP worker, RoadRunner v2025, PHP-FPM + Nginx, FreeUnit, Rapira worker, Rapira classic, Rapira dispatcher, OxPHP classic, OxPHP worker; 2026-10-07: Swoole; 2026-10-08: Apache + mod_php, Workerman, ReactPHP, Amp; 2026-10-13: RoadRunner v3.
Run notes
Run note (2026-10-05). PHP 8.5.11, FrankenPHP 1.13.0, FreeUnit 1.37.0; opcache.enable_file_override=1.
Run note (2026-10-05). Unrelated development containers were running on the benchmark host.
Run note (2026-10-05). All six Rapira scenarios recorded socket-read errors.
Run note (2026-10-05). Raising PostgreSQL max_connections from 200 to 2000 eliminated the earlier OxPHP worker database failures. Its highest 30-second connection sample was 441, including the monitor.
Run note (2026-10-07). Swoole 6.2.3 uses 20 workers in process mode, asynchronous reloads after 10,000 requests, and disabled coroutine request handling and compression. PHP and PostgreSQL settings match the October 5 runs.
Run note (2026-10-08). Apache 2.4.68 with mod_php uses 20 prefork processes and disabled keepalive; Workerman 5.2.2 uses 20 synchronous persistent Yii workers with Event 3.1.6. Both recycle after 10,000 requests/connections. These are a separate October 8 session using the same PHP 8.5.11, PostgreSQL 2,000-connection limit and load settings as the October 5 and 7 runs. Other runtimes were not rerun.
Run note (2026-10-08). The Apache database ramp and Workerman ramps include socket read errors. Workerman closes persistent connections when recycling workers. Raw error counts are retained and reduce successful RPS using the same calculation as the existing runs; neither runtime logged PHP application errors during measurement.
Run note (2026-10-08). React HTTP 1.11.1 uses a return-type-only PSR-7 v2 compatibility patch isolated to its Docker image; Amp HTTP Server is 3.4.6 with Revolt 1.0.9. Both use 20 Supervisor-managed PHP processes, SO_REUSEPORT, TCP_NODELAY, Event 3.1.6, and recycling after 10,000 requests. Yii and PDO handling remains synchronous, with per-request state reset and native request concurrency limited to one per process. This separate October 8 session uses the same PHP 8.5.11 and load settings as the earlier measurements; existing runtimes were not rerun.
Run note (2026-10-08). Both runtimes recorded socket read errors while recycling workers. Amp also recorded non-success HTTP responses; its native graceful shutdown can send 503 responses to queued requests. All recorded errors are retained and reduce successful RPS using the existing report calculation. No PHP application errors were logged during measurement.
Run note (2026-10-13). RoadRunner v3 is 3.0.0, measured on October 13 with PHP 8.5.11, 20 workers, PostgreSQL max_connections=2000, and the same 32-thread, 256-connection ramp settings as the earlier sessions. RoadRunner v2025 retains the October 5 measurements of 2025.1.15; these are separate sessions with different background activity and Docker versions, not a simultaneous controlled A/B test.
Run note (2026-10-13). RoadRunner v3 keeps configuration version 3 and reverses the HTTP middleware list to [gzip, static] to preserve the v2025 execution order. Both versions use the same locked PHP dependencies and worker entry point; the executable is supplied by the corresponding pinned Docker image, independently of the Composer spiral/roadrunner metapackage.
Run note (2026-10-13). Both RoadRunner v3 endpoint ramps completed all 13 stages with zero recorded request errors. No PHP application errors were logged. Throughput saturation and overload latency remain visible in the report.
Charts show raw samples. For stage results, RPS cap marks the first stage with successful throughput more than 5% below target; per-second results use a sustained latency surge. The load generator can also limit throughput.
Hover over a chart legend label or focus it with Tab to highlight that run.
Run Summary — Non-DB
Normal latency averages samples before a detected latency surge or throughput cap, whichever comes first. Overloaded latency comes from the throughput-cap sample shown in the RPS columns. Runs may include different load levels in their normal averages, so these are not equal-load comparisons. Mean p95 averages the included sample percentiles, not pooled requests. — means no qualifying measurement.
Calculation details
For stage data, each sample represents one load stage. The first stage provides the latency baseline. A surge is detected when average latency reaches the largest of 20 ms, 4× baseline average latency, or baseline average latency + 12 ms. When baseline p95 is available, the stage must also have a p95 reaching the largest of 30 ms, 2.5× baseline p95, or baseline p95 + 15 ms. These thresholds are a heuristic, not a latency guarantee.
Normal averages exclude the first qualifying surge stage and all later stages, or stop earlier at the throughput cap. If the cap occurs in the first stage, no normal measurement is available. If no cap is reached, no overloaded measurement is available. For per-second data, the existing sustained latency-surge detector determines the cutoff, and averages use per-second samples.
| Workerman | / | 124311 | 150000 | 1.38 ms | 3.26 ms | 3390.00 ms | 5390.34 ms |
| Swoole | / | 116192 | 150000 | 1.17 ms | 2.04 ms | 4423.15 ms | 6684.67 ms |
| Rapira dispatcher | / | 111499 | 150000 | 1.07 ms | 1.81 ms | 5088.10 ms | 8413.18 ms |
| Rapira worker | / | 102703 | 150000 | 1.17 ms | 1.96 ms | 6242.32 ms | 10010.62 ms |
| ReactPHP | / | 84441 | 100000 | 1.38 ms | 2.44 ms | 3054.75 ms | 6139.90 ms |
| OxPHP worker | / | 79785 | 100000 | 3.44 ms | 14.68 ms | 4074.05 ms | 5898.24 ms |
| FrankenPHP worker | / | 66624 | 75000 | 1.48 ms | 2.51 ms | 2247.96 ms | 3237.89 ms |
| Amp | / | 55373 | 75000 | 1.74 ms | 3.24 ms | 4938.95 ms | 9641.98 ms |
| RoadRunner v3 | / | 45599 | 50000 | 1.56 ms | 2.71 ms | 1836.23 ms | 2555.90 ms |
| RoadRunner v2025 | / | 45258 | 50000 | 1.81 ms | 3.15 ms | 1880.49 ms | 2756.61 ms |
| FreeUnit | / | 11445 | 15000 | 4.80 ms | 9.57 ms | 4743.17 ms | 6877.18 ms |
| FrankenPHP classic | / | 10562 | 15000 | 5.52 ms | 17.24 ms | 5896.74 ms | 8585.22 ms |
| PHP-FPM + Nginx | / | 10264 | 15000 | 7.18 ms | 12.03 ms | 6326.52 ms | 9166.85 ms |
| Rapira classic | / | 9138 | 10000 | 4.67 ms | 10.10 ms | 2304.96 ms | 7647.23 ms |
| Apache + mod_php | / | 8926 | 10000 | 4.60 ms | 9.93 ms | 1844.82 ms | 3108.86 ms |
| OxPHP classic | / | 8749 | 10000 | 5.41 ms | 10.79 ms | 2470.19 ms | 3614.72 ms |
Run Summary — DB
Normal latency averages samples before a detected latency surge or throughput cap, whichever comes first. Overloaded latency comes from the throughput-cap sample shown in the RPS columns. Runs may include different load levels in their normal averages, so these are not equal-load comparisons. Mean p95 averages the included sample percentiles, not pooled requests. — means no qualifying measurement.
Calculation details
For stage data, each sample represents one load stage. The first stage provides the latency baseline. A surge is detected when average latency reaches the largest of 20 ms, 4× baseline average latency, or baseline average latency + 12 ms. When baseline p95 is available, the stage must also have a p95 reaching the largest of 30 ms, 2.5× baseline p95, or baseline p95 + 15 ms. These thresholds are a heuristic, not a latency guarantee.
Normal averages exclude the first qualifying surge stage and all later stages, or stop earlier at the throughput cap. If the cap occurs in the first stage, no normal measurement is available. If no cap is reached, no overloaded measurement is available. For per-second data, the existing sustained latency-surge detector determines the cutoff, and averages use per-second samples.
| Swoole DB | /postgres/orders | 13645 | 15000 | 4.06 ms | 8.03 ms | 1804.08 ms | 2992.13 ms |
| Workerman DB | /postgres/orders | 13501 | 15000 | 4.18 ms | 8.41 ms | 1921.93 ms | 4968.45 ms |
| Rapira dispatcher DB | /postgres/orders | 12907 | 15000 | 4.11 ms | 9.34 ms | 2719.69 ms | 7344.13 ms |
| FrankenPHP worker DB | /postgres/orders | 12709 | 15000 | 4.05 ms | 7.64 ms | 3033.66 ms | 4415.49 ms |
| OxPHP worker DB | /postgres/orders | 12705 | 15000 | 4.09 ms | 7.24 ms | 2981.39 ms | 4419.58 ms |
| Rapira worker DB | /postgres/orders | 12569 | 15000 | 4.04 ms | 8.84 ms | 3090.07 ms | 8372.22 ms |
| ReactPHP DB | /postgres/orders | 10946 | 15000 | 6.95 ms | 15.41 ms | 5147.31 ms | 10960.90 ms |
| RoadRunner v3 DB | /postgres/orders | 10723 | 15000 | 4.32 ms | 7.67 ms | 5734.59 ms | 8269.82 ms |
| RoadRunner v2025 DB | /postgres/orders | 10619 | 15000 | 5.03 ms | 10.50 ms | 5839.41 ms | 8470.53 ms |
| Amp DB | /postgres/orders | 8541 | 10000 | 7.39 ms | 17.18 ms | 1897.80 ms | 6262.78 ms |
| FreeUnit DB | /postgres/orders | 6217 | 10000 | 6.20 ms | 9.25 ms | 7527.98 ms | 10960.90 ms |
| FrankenPHP classic DB | /postgres/orders | 6029 | 10000 | 9.07 ms | 17.98 ms | 7977.87 ms | 11526.14 ms |
| PHP-FPM + Nginx DB | /postgres/orders | 5806 | 10000 | 9.74 ms | 18.49 ms | 8373.99 ms | 12156.93 ms |
| OxPHP classic DB | /postgres/orders | 5044 | 10000 | 12.92 ms | 21.26 ms | 9893.81 ms | 14360.58 ms |
| Rapira classic DB | /postgres/orders | 4713 | 5000 | 9.59 ms | 21.71 ms | 590.21 ms | 2179.07 ms |
| Apache + mod_php DB | /postgres/orders | 2025 | 2500 | — | — | 388.38 ms | 2306.05 ms |