Benchmarks
Daravel publishes a number only with the script that produced it, the machine it ran on and the raw measurements (BLUEPRINT_V2 P10). This page compares Daravel with Laravel 12 doing the same work. The harness and the internal targets are in Performance.
What is compared#
Three servers answer the same six endpoints, on the same database and the same cores:
- Daravel: the AOT build, with 1 and 4 worker isolates.
- Laravel Octane: Laravel 12 on PHP 8.4 with OPcache, on FrankenPHP's worker mode (the application stays in memory between requests), with 1 and 4 workers.
- Laravel classic: the same Laravel on FrankenPHP without worker mode, which boots the application for every request, as php-fpm does.
| Scenario | Endpoint | Work |
|---|---|---|
| framework overhead | GET /ping |
a text response, no database |
| JSON + one query | GET /api/users/42 |
findOrFail, the model as JSON |
| validation + insert | POST /api/users |
two rules, an insert in a transaction |
| eager loading | GET /api/posts |
20 posts with their author and 10 comments each |
| rate limited route | GET /api/throttled |
throttle with a limit never reached |
| login | POST /api/login |
find a user, verify a bcrypt hash (cost 10) |
The Laravel endpoints in bench/laravel/overlay/ mirror
bench/lib/bench_app.dart: the same routes, validation rules, queries and
responses. Laravel's configuration, routes and events are cached, as in
production. Daravel seeds the database (1 000 users, 200 posts, 2 000
comments) and Laravel verifies its $2y$ hashes as they are.
How a published run is made#
bench/public.sh runs everything on one Linux machine with Docker, and
writes the results into bench/results/<date>-<host>/:
- The environment: CPU model, cores and threads per core, memory, virtualisation, CPU governor, kernel, Docker, the digest of the Dart image, PHP, FrankenPHP, Laravel, Octane, PostgreSQL, oha, the commit, and the load and containers on the machine before the run.
- Five runs of every scenario, 10 seconds each after a 2-second warm-up, at 64 keep-alive connections. A table gives the median, with the lowest and highest run beside it, so one noisy run shows as spread instead of moving the number.
- PostgreSQL: the scenarios that query (2 to 4, and the overload of scenario 11), five runs again, against a fresh PostgreSQL 17 server.
- Overload: Daravel alone (Laravel has no equivalent) flooded with
512 connections on scenario 2's endpoint, without a limit, with
HTTP_MAX_IN_FLIGHTand withHTTP_MAX_QUEUE_DELAYadded (load shedding): goodput, p99 of the successful requests and the share refused with503, five runs. Twice: with clients that retry at once (closed loop, scenario 11), and at a fixed arrival rate 1.5 times the capacity from clients that do not (open loop, scenario 12), the load that shedding is meant for. - A saturation curve:
/pingand the JSON scenario at 16, 64, 256 and 1 024 connections, with plaindart:ioas the ceiling for/ping. - A second load generator: oha, written in Rust (pinned version, checksum verified), measures Daravel and Octane on the GET scenarios. If its numbers and the harness's agree, the results do not come from the Dart load generator.
- The raw data: the JSON of every run stays next to
report.mdandsummary.json.
The servers (and PostgreSQL) run on the first half of the cores and the load generators on the second half, so they never compete for a core.
Differences that remain#
- Laravel's rate limiter uses the
arraycache, so each Octane worker counts on its own; Daravel's limit is shared by every isolate through the coordinator, which costs a message per request. /pingis registered outside Laravel'swebmiddleware group (no session, no CSRF), as Daravel's/pinghas none.- On PostgreSQL, Daravel keeps a pool of 4 connections per isolate, while PHP holds one connection per Octane worker, and classic connects for every request. Each is the runtime's normal setup.
- SQLite is a file on the same disk for every server: Daravel's and
Laravel's drivers both use WAL,
busy_timeout5000 andsynchronous=NORMAL.
The first two favour Laravel.
Results#
Published runs#
None yet. A run on a dedicated machine is listed here with its
environment, its tables and a link to its raw data in bench/results/.
Indicative run#
One run on 2026-10-26: WSL2 on a 16-thread laptop, with other containers running, 10 s per scenario, 64 connections, four workers, SQLite. A single run on a shared machine, so read the ratios rather than the absolute numbers.
| Scenario | Daravel | Laravel Octane | Laravel classic |
|---|---|---|---|
GET /ping |
12 160 req/s | 2 254 req/s | 983 req/s |
| JSON + one query | 9 594 req/s | 1 843 req/s | 720 req/s |
| validation + insert | 2 918 req/s | 1 079 req/s | 551 req/s |
| eager loading (20 posts) | 1 876 req/s | 440 req/s | 300 req/s |
| rate limited route | 10 494 req/s | 1 721 req/s | 902 req/s |
| login (bcrypt, cost 10) | 35 req/s (61 since 1.10) | 72 req/s | 68 req/s |
With one worker Daravel served 6 to 10 times Octane's throughput on the
first five scenarios. Login was the exception: package:bcrypt in pure
Dart took 124 ms per hash where PHP's C bcrypt takes 70 ms. Since 1.10
Daravel runs the system's C bcrypt on Linux (see Hashing);
a second run of the login scenario on the same machine gave Daravel
61 req/s, Octane 55 and classic 47, so login costs what it costs in
Laravel.
For WebSocket connections (10 000 Laravel Echo clients on one server), see Broadcasting.
Reading the numbers#
- Throughput and p99 together. Requests per second say how much a server can do; p99 says what the slowest one in a hundred users waits. Under a fixed number of connections, latency is mostly time in the queue, so a faster server also has a lower p99.
- The saturation curve. While connections grow, throughput first rises and then stays flat while p99 keeps climbing: from there on, more clients only wait longer. Errors in the curve are responses of 400 or more and failed connections.
- Per core. Every server gets the same cores. Four workers on four cores is the fair comparison; one worker shows the cost of a single request path.
- Your own hardware. The script needs Linux, Docker and four cores or more. Run it before relying on any number here.
Run it yourself#
git clone https://github.com/daravel-dev/daravel && cd daravel
bash bench/public.sh # about an hour on 8 cores
RUNS=1 DURATION=3 CURVE=16,64 bash bench/public.sh # a quick dry run
cat bench/results/*/report.md
bench/laravel/compare.sh runs a single comparison (DB=pgsql for
PostgreSQL, CONNECTIONS=16,64,256 for several connection counts). The
settings of both are in
bench/README.md.