How much can you speed up Laravel if you handle requests in coroutines instead of blocking workers? We benchmarked TrueAsync (native PHP coroutines) against three Laravel Octane configurations.
The Benchmark
Environment
- OS: WSL2 (Linux 5.15), 16 cores, 7.8 GB RAM
-
DB: PostgreSQL 16 (
max_connections=500) -
Load tool:
k6,constant-arrival-rate, 1,000 req/s for 30 seconds
| Parameter | TrueAsync FrankenPHP | Octane Swoole (NTS) | Octane Swoole (ZTS) | Octane FrankenPHP |
|---|---|---|---|---|
| PHP | 8.6.0-dev (ZTS) | 8.5.4 (NTS) | 8.5.4 (ZTS) | 8.5.4 (NTS) |
| Server | FrankenPHP (true-async fork) | Swoole 6.2.0 | Swoole 6.2.0 | FrankenPHP (official) |
| Laravel | 13.2.0 | 13.2.0 | 13.2.0 | 13.2.0 |
| Model | Coroutines (libuv) | Processes (fork) | Threads (ZTS) | Processes (fork) |
Note: Swoole runs without coroutine mode here because Laravel is not adapted for it. In a pure synthetic test with coroutines, Swoole shows slightly better numbers than FrankenPHP + TrueAsync. Both servers reach ~10,000 req/s with 12 workers on synthetic loads.
Full benchmark repository: github.com/YanGusik/ta_benchmark
Workload
The /bench endpoint executes 10 sequential SQL queries against PostgreSQL: user lookup, post listing, INSERT a view record, UPDATE a counter, aggregations, TOP-N selections. Database: 100 users, 1,000 posts, growing post_views table.
This is a realistic workload, not a synthetic "Hello World".
Results
Throughput (req/s)
| Workers | TrueAsync | Swoole NTS | Swoole ZTS | FrankenPHP Octane |
|---|---|---|---|---|
| 4 | 989 | 183 | 185 | 189 |
| 8 | 993 | 342 | 341 | 346 |
| 12 | 990 | 483 | 476 | 489 |
| 16 | 987 | 599 | 601 | 556 |
With 16 workers, TrueAsync handles 987 req/s. The best Octane result is 601 req/s (Swoole ZTS), 64% less with the same worker count.
We gave blocking servers 16 workers to be generous. TrueAsync doesn't need them. Four workers handle 989 req/s, the same as sixteen. Coroutines yield on every PDO::query(), so one worker runs dozens of requests concurrently. While one coroutine waits for PostgreSQL, others keep working.
Median Latency (P50)
| Workers | TrueAsync | Swoole NTS | Swoole ZTS | FrankenPHP Octane |
|---|---|---|---|---|
| 4 | 28 ms | 5,440 ms | 5,320 ms | 5,240 ms |
| 8 | 27 ms | 2,870 ms | 2,900 ms | 2,800 ms |
| 12 | 28 ms | 2,040 ms | 2,050 ms | 1,990 ms |
| 16 | 29 ms | 1,640 ms | 1,660 ms | 1,780 ms |
29 ms vs 1,640 ms at 16 workers. 56x difference. Where do those seconds come from?
| Phase | TrueAsync (4w) | Swoole (4w) |
|---|---|---|
| PHP execution | ~5 ms | ~5 ms |
| SQL I/O wait (10 queries) | ~23 ms | ~23 ms |
| Queue wait | ~0 ms | ~5,400 ms |
| Total | ~28 ms | ~5,440 ms |
PHP and SQL run at identical speeds. The entire difference is queue wait: a blocking server can't start your request until the current one finishes. With TrueAsync, CPU utilization is higher because coroutines yield during I/O instead of blocking the worker.
No magic. Just better resource utilization.
Memory (under load)
| Workers | TrueAsync | Swoole ZTS | FrankenPHP Octane |
|---|---|---|---|
| 4 | 277 MB | 508 MB | 401 MB |
| 8 | 286 MB | 600 MB | 417 MB |
| 16 | 308 MB | 765 MB | 403 MB |
In the blocking model, each worker is a separate process with a full Laravel copy: container, configuration, router, middleware, database manager. With TrueAsync, coroutines share a common bootstrap. Only per-request data (request, session, auth) is duplicated. Hence 308 MB vs 765 MB at 16 workers.
Key Takeaways
Tests are not something you should fully trust. Different scenarios are possible. However, thereโs no magic here.
Even at maximum workers, TrueAsync wins by 30-40%. You could spin up 22-27 blocking workers to match throughput, but you'd still lose on latency and memory. And why use 22 workers when 4 will do?
The bottom line: for IO-bound workloads (which is most web apps), TrueAsync serves the same traffic with 5-6x fewer workers, 56x lower latency, and half the memory.


Top comments (0)