Framework Competition — RoadRunner Summary

The headline numbers from the measured RoadRunner deployment: the cost of one pass over every endpoint, the plain routing request, the two data-access races, and the worker memory that survives a request. End-to-end HTTP over loopback, so the constant webserver overhead is included (see floor-rr in the dataset). The deployment model actually measured is stated with the table.

PHP 8.3.33 · Linux 6.8.0-139-generic · roadrunner mode · OPcache: yes · 1000 iterations per run over 10 runs · charts show the median as a faint bar + dot with the fastest → p95 range, lower is better

Boot model: the worker is never recycled (max_jobs=0), so no boot is added to these requests; the one-time recycle is measured in the full report

Total response times
Total response times
Resident worker memory
Resident worker memory

Feature benchmarks

Routing

GET / — dispatches a plain request through the router and returns a rendered template — no database access.

Routing
ORM / Active Record

GET /items — loads one page of the 1,000 seeded rows through each framework's ORM / Active Record layer: 20 items plus a COUNT for the pagination total.

ORM / Active Record
REST API (JSON)

GET /api/items — serves the same page of items as a JSON response rather than HTML, which adds serialization to the ORM work.

REST API (JSON)

Latency by endpoint (ms, trimmed mean — lower is better) — the worker is never recycled (max_jobs=0), so each number is the measured request with no boot in it

RequestWorkloadAzeraLaravelSymfonySpiralCodeIgniterCakePHP
GET /no DB — routing + template only0.2600.5970.4240.6670.8930.486
GET /items20 of 1000 items (page 1, + COUNT)0.4941.221.071.011.260.964
GET /items/11 item by id0.3400.8510.6010.7911.120.710
POST /items1 row upserted (sentinel #999999)0.4710.8850.7880.8531.210.875
GET /items-qb20 of 1000 items (page 1, + COUNT)0.4480.9010.6700.8131.230.761
GET /items-qb/11 item by id0.3410.7490.5040.7621.130.655
POST /items-qb1 row upserted (sentinel #999997)0.4340.8690.6940.8101.340.730
GET /api/items20 of 1000 items as JSON0.3231.070.7080.8521.070.670
GET /api/items/11 item by id as JSON0.2970.8540.5220.7321.030.651
POST /api/items1 row upserted (sentinel #999998)0.3290.7910.7340.8191.120.771
GET /features/aopno DB — interceptor pipeline0.4410.7850.6240.949——
GET /features/cacheCOUNT(*) of 1000 rows, cached 10s (miss = query)0.2330.6230.3760.6940.8160.407
GET /features/logno DB — buffered log handlers0.2370.5800.3870.691——
GET /features/retryno DB — retry policy0.2270.6080.3560.703——
GET /features/pipelineno DB — middleware pipeline0.2440.5940.3900.700——
GET /features/db-events1 event row INSERTed per request0.3470.8410.6110.9391.200.681
GET /features/eventsno DB — in-process listeners0.3260.7460.4500.7971.170.539
GET /features/validationno DB — validator run0.2571.310.5380.7291.120.512
GET /features/configno DB — config lookup0.2290.5920.3990.6780.8380.360
GET /features/request-scopedno DB — scoped service resolve0.2300.5850.3860.7240.8170.376
GET /features/rate-limitno DB — cache-backed limiter0.2460.5850.3600.7250.8240.393

Server floor — real RoadRunner over loopback: a bare resident worker that renders a fixed string (floor-rr) measures the IPC + server floor every row below also pays. Only differences larger than this floor are framework differences.