Framework Competition — Real PHP-FPM

Real nginx + PHP-FPM serving over HTTP: the framework boots for every request, which is what PHP actually runs in production. The pool's worker-recycling setting is stated with the server floor below, since it changes what each row contains. End-to-end HTTP over loopback — includes the constant webserver overhead (see floor-http/floor-php in the dataset). Single sequential client.

PHP 8.3.33 · Linux 6.8.0-139-generic · php-fpm 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

Framework startup — per-request boot (PHP-FPM)
Framework startup — per-request boot (PHP-FPM)
Total response times
Total response times
Per-request memory — lightest, typical and heaviest endpoint
Per-request memory — lightest, typical and heaviest endpoint

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
Query Builder

GET /items-qb — builds the same page of 20 items with each framework's query builder instead of its ORM, so the two data-access styles can be compared directly.

Query Builder
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)
AOP (Aspect-Oriented)

GET /features/aop — runs a request through an interceptor pipeline — logging, retry and middleware aspects wrapped around the handler. Only frameworks with an AOP layer take part.

AOP (Aspect-Oriented)
Cache

GET /features/cache — reads a COUNT(*) over the 1,000 rows through the framework's cache with a 10-second TTL, so a hit costs no database work and a miss runs the query.

Cache
Database Events

GET /features/db-events — inserts one event row per request and lets the framework's database events fire around that write.

Database Events
Event Dispatcher

GET /features/events — dispatches an in-process event to registered listeners.

Event Dispatcher
Validation

GET /features/validation — validates a payload with the framework's own validator.

Validation
Config

GET /features/config — resolves a value from the framework's config repository.

Config
Request-Scoped Services

GET /features/request-scoped — resolves a service scoped to the request from the container.

Request-Scoped Services
Rate Limiter

GET /features/rate-limit — checks a cache-backed rate limiter.

Rate Limiter

Latency by endpoint (ms, trimmed mean — lower is better)

RequestWorkloadAzeraLaravelSymfonySpiralCodeIgniterCakePHP
GET /no DB — routing + template only0.8104.512.107.931.971.53
GET /items20 of 1000 items (page 1, + COUNT)1.585.743.768.992.722.89
GET /items/11 item by id1.445.403.048.882.612.69
POST /items1 row upserted (sentinel #999999)1.645.403.928.962.712.85
GET /items-qb20 of 1000 items (page 1, + COUNT)1.405.242.668.512.692.30
GET /items-qb/11 item by id1.345.112.588.452.592.20
POST /items-qb1 row upserted (sentinel #999997)1.445.173.478.652.822.32
GET /api/items20 of 1000 items as JSON1.386.093.038.012.542.58
GET /api/items/11 item by id as JSON1.415.882.777.892.522.56
POST /api/items1 row upserted (sentinel #999998)1.475.283.608.032.642.71
GET /features/aopno DB — interceptor pipeline2.525.993.279.66——
GET /features/cacheCOUNT(*) of 1000 rows, cached 10s (miss = query)1.665.362.988.772.502.42
GET /features/logno DB — buffered log handlers1.234.521.958.06——
GET /features/retryno DB — retry policy1.224.621.978.06——
GET /features/pipelineno DB — middleware pipeline0.8114.561.988.07——
GET /features/db-events1 event row INSERTed per request1.755.433.169.002.792.64
GET /features/eventsno DB — in-process listeners1.735.122.418.672.691.94
GET /features/validationno DB — validator run0.8235.622.298.182.291.84
GET /features/configno DB — config lookup0.7654.591.968.071.971.40
GET /features/request-scopedno DB — scoped service resolve0.7704.591.988.071.951.41
GET /features/rate-limitno DB — cache-backed limiter0.7724.732.008.161.961.51

Server floor — real nginx + PHP-FPM with pm.max_requests=0: the pool never recycles its worker, so no process is spawned per request — what remains is the FastCGI handshake plus a minimal script. A hello-world endpoint that boots nothing but PHP (floor-php) and a static file through nginx (floor-http) measure exactly that cost — the floor every row stands on. Subtracting that floor leaves the framework's own per-request boot, which FPM still pays for every request even though its worker survives.