In our previous benchmark, Swoole finished last in a light I/O workload, challenging its reputation as a performance monster. Readers immediately asked: “Is Swoole really that slow, or did the test simply not play to its strengths?” Today, we answer that question definitively—by putting all four application servers through escalating database latency and watching the leaderboard shift in real time.
We tested PHP-FPM, RoadRunner, FrankenPHP, and Swoole under three levels of simulated database latency: 50ms, 200ms, and 500ms. The question was simple: when queries slow down, which runtime survives and which collapses?
This I/O latency test builds on the baseline established in our earlier application server performance under I/O load comparison. We recommend reading that first for the full context on how each runtime handles light database queries.
The answer is nuanced. RoadRunner leads at every level, Swoole delivers the promised comeback, and FrankenPHP struggles unexpectedly under heavy I/O load.

Why I/O Latency Changes Everything
Before diving into the numbers, it is essential to understand why database latency is the single most important variable in application server performance.
In a light I/O scenario—such as a SELECT 1 query that returns in under a millisecond—the bottleneck is almost entirely CPU. The runtime that can execute PHP code fastest wins. This is why Swoole finished last in our previous test: its coroutine scheduler adds a tiny overhead that, in the absence of any meaningful I/O wait, acts as a net negative.
In a heavy I/O scenario, everything changes. When a query takes 200ms, the worker process sits idle for 200ms waiting for the database. A traditional process model like FPM simply blocks—one process, one waiting request. A coroutine model like Swoole can switch to another request during that wait, handling dozens of concurrent connections with a single worker.
This is why we expected Swoole to climb the leaderboard as latency increased. The data proved us right—but with important caveats.
Test Environment & Methodology
All tests ran on a single Alibaba Cloud ECS instance to eliminate network variance. The software stack mirrors a typical production environment managed by the Baota panel.
For context on our testing methodology, see our first benchmark and our previous application server comparison .
Server Specifications
- OS: Ubuntu 24.04 LTS
- Web Server: Nginx 1.26 (for PHP‑FPM only)
- PHP Versions: 8.5.2 (FPM, RoadRunner, Swoole) / 8.5.5 (FrankenPHP built‑in)
- Database: MySQL 8.4
- Control Panel: Baota (for convenient PHP‑FPM management)
Four Environments, One Configuration
| Environment | Port | PHP Version | Worker/Process Count |
|---|---|---|---|
| PHP‑FPM | 80 | 8.5.2 | 20 (static) |
| RoadRunner | 8081 | 8.5.2 | 20 |
| FrankenPHP | 8082 | 8.5.5 | 19 (threads) |
| Swoole | 8083 | 8.5.2 | 20 (coroutines) |
Verified before testing: ps aux confirmed 20 FPM processes, 20 RoadRunner workers, 19 FrankenPHP threads, and 27 Swoole processes (including 20 coroutine workers).
Uniform PHP Configuration
All four environments shared identical PHP settings. JIT was kept disabled (the PHP 8.5 default) to isolate runtime architecture impact.
| Directive | Value |
|---|---|
memory_limit | 256M |
opcache.enable | On |
opcache.validate_timestamps | Off |
opcache.memory_consumption | 512 |
opcache.jit | Off |
opcache.jit_buffer_size | 0 |
Three Latency Levels
We created four routes in a Laravel 11 application, each performing 1,000 sqrt() calculations (light CPU pressure) followed by a database query with a controlled delay:
| Route | SQL | I/O Delay |
|---|---|---|
/bench-slow | SELECT SLEEP(0.05) as ok | 50ms |
/bench-xslow | SELECT SLEEP(0.2) as ok | 200ms |
/bench-ultra | SELECT SLEEP(0.5) as ok | 500ms |
Load Testing Tool & Parameters
- Tool: ApacheBench (ab) 2.3
- Concurrency: 100 (
-c 100) - Duration: 60 seconds per run (
-t 60) - Strategy: Each environment ran 3 consecutive rounds with a 10-second warm‑up before each measurement. The first round was discarded; the final result is the average of rounds 2 and 3.
Benchmark Results
The table below shows the average RPS (Requests Per Second) and P99 latency for each environment across all three I/O delay levels.
| Environment | 50ms RPS | 200ms RPS | 500ms RPS | 50ms P99 | 200ms P99 | 500ms P99 |
|---|---|---|---|---|---|---|
| PHP‑FPM | 309.3 | 96.2 | 38.9 | 388ms | 1081ms | 2598ms |
| RoadRunner | 392.3 | 98.9 | 39.3 | 259ms | 1015ms | 2519ms |
| FrankenPHP | 139.7 | 38.3 | 15.3 | 773ms | 2708ms | 6653ms |
| Swoole | 372.7 | 98.1 | 39.2 | 589ms | 1064ms | 2587ms |
Key Takeaways
RoadRunner leads at every latency level. It posted the highest RPS across all three scenarios—from a 5.3% advantage over Swoole at 50ms to a slim 0.3% edge at 500ms. Its Go-based goroutine scheduler proves remarkably efficient regardless of I/O wait time.
Swoole delivers the promised comeback. After finishing last in our light I/O test, Swoole climbed to second place when I/O latency reached 50ms and maintained that position through 500ms. The coroutine model works exactly as advertised—it just needs meaningful I/O wait time to unlock its potential.
PHP‑FPM holds its ground. Despite the architectural disadvantage of a blocking process model, FPM remained within striking distance of the leaders at every latency level. At 500ms, its RPS (38.9) was only 1% behind RoadRunner (39.3). This is a testament to the maturity and stability of the FPM process pool.
FrankenPHP struggles under heavy I/O. At 50ms, FrankenPHP was already at half the throughput of the leaders. At 500ms, its RPS collapsed to 15.3—only 39% of FPM’s throughput—with a staggering P99 latency of 6.6 seconds. This suggests that FrankenPHP’s dynamic thread scaling does not handle long-blocking operations efficiently in its default configuration.
Deep Analysis: Who Wins When It Matters?
The RoadRunner Pattern: Steady Dominance
RoadRunner’s consistent lead across all latency levels stems from three architectural advantages:
- Goroutine scheduler efficiency. Go’s scheduler handles blocked goroutines with minimal overhead, allowing RoadRunner to maintain high throughput even when workers are waiting on database responses.
- Memory-resident framework. Laravel boots once per worker and reuses that state, eliminating the repeated initialization cost that FPM pays on every request.
- Binary protocol communication. The Goridge protocol between Go and PHP workers minimizes serialization overhead, preserving CPU cycles for actual request processing.
These advantages compound to produce a runtime that is fast at low latency and resilient at high latency—a combination no other environment matched in this test.
The Swoole Comeback: Coroutines Earn Their Keep
Swoole’s rise from last place to second place is the most compelling narrative in this data set. At 50ms I/O delay, Swoole jumped from 0.9% behind FPM to 20.5% ahead. At 200ms, it held a narrow lead over FPM. At 500ms, it was essentially tied with RoadRunner.
This confirms the central hypothesis of our two-article series: Swoole’s coroutine advantage requires significant I/O wait time to manifest. The SELECT 1 query in our previous test offered virtually no blocking time for the coroutine scheduler to exploit. As latency increases, the scheduler finds more opportunities to switch between coroutines, turning idle wait time into productive throughput.
For developers operating applications with slow database queries, external API calls, or other high-latency I/O operations, Swoole should now be a top consideration.
The FPM Surprise: Old School Still Works
PHP-FPM deserves credit for its remarkably stable performance curve. Its RPS decayed smoothly from 309.3 at 50ms to 38.9 at 500ms—a predictable decline that mirrors the theoretical maximum of a 20-process pool handling 500ms blocking operations (20 / 0.5 = 40 RPS).
The fact that FPM nearly matched RoadRunner and Swoole at 500ms suggests that for extreme blocking scenarios, the raw math of process count × blocking time dominates over any architectural cleverness. If you have exactly 20 processes and each request blocks for exactly 500ms, no scheduler can exceed 40 RPS—and FPM hit 38.9, or 97% of the theoretical ceiling.
The FrankenPHP Mystery: Why Did It Struggle?
FrankenPHP’s poor showing at high latency levels demands explanation. With 19 threads—nearly identical to the other environments’ worker count—it should theoretically perform similarly.
The likely culprit is dynamic thread scheduling overhead. FrankenPHP’s Caddy server spawns and manages PHP threads dynamically based on load. Under sustained high-concurrency blocking I/O, this dynamic scaling may introduce latency spikes as threads are created, destroyed, and reallocated. The P99 latency of 6.6 seconds at 500ms strongly suggests that some requests waited through multiple scheduling cycles before being served.
For production use of FrankenPHP in I/O-heavy workloads, we recommend:
- Testing with the latest version that supports the
workerdirective for fixed thread pools. - Experimenting with the
FRANKENPHP_WORKERSenvironment variable to eliminate dynamic scaling. - Using HTTP/2-capable load testing tools like
bombardierorwrkfor more representative benchmarks.
Resource Consumption Observations
While the benchmark focused on RPS and latency, we also monitored CPU and memory usage via htop during the tests.
- PHP‑FPM: Maintained exactly 20 processes, each consuming approximately 30‑50 MB of RAM. Total memory footprint was approximately 600‑1000 MB. CPU usage was distributed evenly across all processes.
- RoadRunner: Showed one Go master process plus 20 PHP worker processes. Memory footprint was slightly lower than FPM due to shared master process overhead. CPU usage was heavily concentrated on the Go master process.
- FrankenPHP: Ran as a single multi‑threaded process. Memory usage was the most compact (approximately 200‑300 MB), but CPU usage spiked unevenly during high-latency tests, suggesting thread contention.
- Swoole: Maintained a single master process with internal coroutine workers. Memory footprint was comparable to FrankenPHP. CPU usage was smooth and predictable across all latency levels, confirming the efficiency of its coroutine scheduler.
Recommendations for Production
Based on the complete picture now available from both our light I/O and heavy I/O benchmarks, here is our updated guidance for choosing a PHP 8.5 runtime.
| Scenario | Recommended Runtime | Reason |
|---|---|---|
| Light I/O, high throughput | RoadRunner | Best all-around RPS, excellent P99 latency |
| Mixed I/O, traditional apps | PHP‑FPM | Stable, predictable, battle-tested |
| Heavy I/O, slow databases | RoadRunner or Swoole | Both handle blocking I/O efficiently |
| External API calls, WebSocket | Swoole | Native coroutine I/O, WebSocket support |
| Containerized, low memory | FrankenPHP | Compact memory footprint |
| Maximum stability, any load | PHP‑FPM | Near-theoretical performance ceiling at high latency |
Full Reproducibility
At PHPBenchLab, we believe benchmarks should be transparent and repeatable. All test scripts, configuration files, and raw data are publicly available to enable independent verification of these results.
Test Endpoints (Publicly Accessible)
- PHP‑FPM:
https://phpbenchlab.com/bench-slow(andbench-xslow,bench-ultra) - RoadRunner:
https://phpbenchlab.com:8081/bench-slow - FrankenPHP:
https://phpbenchlab.com:8082/bench-slow - Swoole:
https://phpbenchlab.com:8083/bench-slow
Benchmark Command (after warm‑up)
bash
ab -c 100 -t 60 http://your-server-ip/bench-slowFrequently Asked Questions
The key difference is I/O latency. In our previous test, the SELECT 1 query executed in under 1 millisecond, leaving no idle time for Swoole’s coroutine scheduler to exploit. In this test, the SELECT SLEEP(0.05) through SELECT SLEEP(0.5) queries created substantial I/O wait windows, allowing Swoole to switch between coroutines during blocking periods and handle far more concurrent requests than a blocking process model.
FrankenPHP’s default dynamic thread scaling likely caused the poor showing. With 19 threads under sustained blocking I/O, thread scheduling overhead accumulated into significant latency spikes. We believe FrankenPHP with a fixed worker count of 20 (via the FRANKENPHP_WORKERS environment variable) would perform significantly better. We plan to revisit this in a future article.
This is a mathematical inevitability rather than an architectural triumph. With 20 processes each handling one blocking 500ms request, the theoretical maximum throughput is 40 RPS. FPM achieved 38.9 RPS—97% of that ceiling. No scheduler, no matter how clever, can exceed the physical limit of processes × blocking time. At extreme blocking levels, all runtimes converge toward this hard ceiling.
RoadRunner offers the best balance of performance at all latency levels. Swoole is equally capable and adds native coroutine support for WebSocket and async I/O. PHP‑FPM is the safest choice if stability and predictability matter more than maximizing throughput. The differences at 500ms are negligible, so your choice should depend on your other requirements (deployment complexity, memory constraints, monitoring, etc.).