# Seven PHP frameworks under identical load: the gap narrows as soon as the request does real work

> Seven frameworks, one PHP build, one server stack and a shared fifty-route contract. Each at its latest stable release, in isolated containers, with the running order reshuffled every repetition.

- Kind: Measurement
- Question: On the same hardware, the same PHP build and the same seven routes, how many requests a second do Laravel, Symfony, CodeIgniter, Yii2, Phalcon, Laminas and Slim serve, and at what latency?
- Finding: On an empty route the fastest is 4.4× the slowest (Slim 25,975, Laravel 5,966 req/s). As soon as the request does real work the gap closes: 3.7× for a single row from the database, 3.5× for twenty rows. Phalcon is third on an empty route and fifth once a query is involved — being a C extension buys nothing while the process waits on MySQL. And the expensive decision is not the framework: Laravel's own default `web` middleware group takes the same response from 5,858 to 2,176 req/s, so one default costs more than most of the distance between the frameworks.
- Method: All seven candidates ran on a single PHP 8.4.24 build, on byte-identical opcache/php.ini/php-fpm pool settings (pm=static, 8 workers) and one nginx server block. Each was installed from its official skeleton with its own documented production set-up (route/config/view caches, APP_ENV=prod, YII_DEBUG=false, Volt compiled ahead of time) and the same composer flags. All seven register exactly fifty routes with the parameterised one always at position 47. Only one candidate runs at a time; php-fpm sits on cores 0-3, nginx on 4-5, MySQL on 6-7 and the load generator on 8-11, disjoint. Every scenario runs for 15 seconds at 64 connections, nine repetitions, with the candidate order reshuffled each round. A fixed nginx reference that never reaches PHP is sampled before and after every block, recording the host capacity that measurement was given. The reported value is the best of the repetitions: the machine could not be quiesced, and interference can only ever subtract throughput, never push it past what the hardware allows. 546 load measurements, 150 host probes, zero non-200 responses.
- Measured on: 2026-08-20
- Confidence: Medium confidence
- Programme: Service & load
- Environment: PHP 8.4.24 NTS · opcache on · JIT off · Server nginx 1.29.1 + php-fpm · pm=static · 8 workers · Database MySQL 8.4.11 · 10,000 rows · persistent connections · Load generator oha 1.15.0 · 64 connections · 15 s · Hardware Apple M4 Pro · 12 cores · 24 GB · macOS 26.6.1 · Virtualisation Docker Desktop 29.7.2 · 12 vCPU / 7.75 GB · aarch64 · Core split fpm 0-3 · nginx 4-5 · MySQL 6-7 · load 8-11 · Repetitions 9 (+6 top-up) · best run reported
- Technologies: Laravel, Symfony, CodeIgniter, Yii2, Phalcon, Laminas, Slim, PHP, Docker, nginx, MySQL
- Raw data: https://github.com/muhammetsafak/php-framework-bench/tree/main/results/2026-08-20
- Published: 2026-08-20
- Source: https://www.muhammetsafak.com.tr/en/research/php-framework-yuk-testi/
- Language: en-US
- Author: Muhammet Şafak

---
Most PHP framework comparisons publish a number without publishing how it was
produced: which version, which hardware, how many repetitions. What this record
contributes is not the ranking but the fact that the ranking can be
**re-derived**. Every figure below comes out of
[bench/run.sh](https://github.com/muhammetsafak/php-framework-bench) and every
raw run is committed in the repository.

## What was measured

Each candidate ran at its latest stable release, from its official skeleton, in
its own documented production set-up. Exactly one thing varies: the framework.

| Candidate | Version | Data layer | Templates |
| --- | --- | --- | --- |
| Slim | 4.15.2 | none — raw PDO | none — slim/php-view |
| Yii2 | 2.0.55 | ActiveRecord | Yii View |
| Phalcon | 5.19.0 | Phalcon Model | Volt |
| Symfony | 8.1.4 | Doctrine ORM | Twig |
| Laminas | 3.8.0 | laminas-db TableGateway | laminas-view |
| CodeIgniter | 4.7.4 | CI Model | CI View |
| Laravel | 13.26.1 | Eloquent | Blade |

Slim ships neither a data layer nor a template engine, so in those two scenarios it is doing less framework work than the others. Read that row alongside its results.

All seven register **exactly fifty routes**, with the parameterised route always
at position 47, so route resolution is measured in a realistic table rather than
a one-entry one. The scenarios: plain text, JSON, a parameterised route, a
five-layer middleware chain, a hundred-row template (no database), a single row
by primary key, and a twenty-row list.

## What the numbers say

**Requests served per second, across three scenarios**

The bars converge as the request goes from an empty route to a database query: the framework's own overhead stops being the dominant cost once real work is involved.

Kaynak: Best of 9 repetitions, 64 connections, 15 s — bench/run.sh output

|  | Slim | Yii2 | Phalcon | Symfony | Laminas | CodeIgniter | Laravel |
| --- | --- | --- | --- | --- | --- | --- | --- |
| Empty route | 25975 | 19834 | 15021 | 13067 | 8349 | 6431 | 5966 |
| Template (100 rows) | 22780 | 16995 | 10563 | 9355 | 7419 | 5557 | 5290 |
| Database (single row) | 14443 | 9366 | 5222 | 6972 | 5421 | 4527 | 3887 |

The most striking movement is **Phalcon's**. Third on the empty route (15,021),
third again on the template, and **fifth** once a database query is involved
(5,222) — below Symfony. On the twenty-row list it slips further, to sixth
(2,040), behind Laminas and CodeIgniter. A framework written in C gains nothing
while the process is waiting on MySQL; what it gains is where PHP would be
running PHP.

| Candidate | Empty route | JSON | Route (47/50) | Middleware | Template | DB single row | DB 20 rows |
| --- | --- | --- | --- | --- | --- | --- | --- |
| Slim | 25,975 | 25,609 | 24,774 | 24,020 | 22,780 | 14,443 | 4,551 |
| Yii2 | 19,834 | 18,731 | 18,775 | 18,877 | 16,995 | 9,366 | 3,245 |
| Phalcon | 15,021 | 15,161 | 15,061 | 14,906 | 10,563 | 5,222 | 2,040 |
| Symfony | 13,067 | 12,714 | 12,187 | 12,964 | 9,355 | 6,972 | 3,060 |
| Laminas | 8,349 | 8,114 | 8,127 | 8,036 | 7,419 | 5,421 | 2,874 |
| CodeIgniter | 6,431 | 6,233 | 5,960 | 6,037 | 5,557 | 4,527 | 2,494 |
| Laravel | 5,966 | 5,848 | 5,603 | 5,704 | 5,290 | 3,887 | 1,293 |

Requests per second, best of 9 repetitions. Ordered by empty-route throughput.

The closing of the gap fits in one line: the ratio of fastest to slowest is
**4.4×** on an empty route, **3.7×** on a single-row database request and
**3.5×** on a twenty-row one. Phalcon's lead over Laravel falls from 2.5× to
1.3×.

## Latency

**Latency percentiles on the JSON scenario**

All seven are sub-millisecond at p99: at this load none of them feels slow to a user. The difference is how many users the same hardware can carry.

Kaynak: Taken from the repetition that produced the best throughput, 64 connections

|  | Slim | Yii2 | Phalcon | Symfony | Laminas | CodeIgniter | Laravel |
| --- | --- | --- | --- | --- | --- | --- | --- |
| p50 | 0.25 ms | 0.35 ms | 0.46 ms | 0.58 ms | 0.88 ms | 1.16 ms | 0.75 ms |
| p95 | 0.43 ms | 0.59 ms | 0.73 ms | 0.89 ms | 1.52 ms | 1.84 ms | 1.97 ms |
| p99 | 0.53 ms | 0.73 ms | 0.94 ms | 0.96 ms | 2.23 ms | 1.98 ms | 2.59 ms |

Laravel's p50 (0.75 ms) is **better** than Laminas's and CodeIgniter's, while its
p99 (2.59 ms) is the worst in the field. Laravel finishes the typical request
quickly and handles the tail badly.

## The arguments are cheap, the defaults are not

Resolving a parameterised route in a fifty-route table costs between **0% and 7%**
against an empty route. A five-layer middleware chain costs **1% to 8%**. The two
things developers argue about most when comparing frameworks turn out to be
single-digit percentages.

Then you look at what Laravel ships by default:

| Laravel route | req/s | p50 | p99 |
| --- | --- | --- | --- |
| No middleware group | 5,858 | 1.32 ms | 2.88 ms |
| Default web group | 2,176 | 1.73 ms | 71.89 ms |

The same JSON response, the same container, the same run. The only difference is the route sitting behind the web group. p99 goes up twenty-five fold.

This was measured with the **file** session driver. Laravel's shipped default is
`database`, which adds a read and a write per request on top. The real figure is
worse than the one above.

> **Sonuç**
>
> Choosing a framework can buy you up to a 4× difference. One default middleware
> group can take 63% away. Before reading the comparison table, look at what sits
> behind your own route.

## Saturation: the ceiling is eight workers

Raising the connection count from 8 to 256 left throughput and p99 **flat** for
all seven.

| Connections | Slim | Yii2 | Phalcon | Symfony | Laminas | CodeIgniter | Laravel |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 8 | 25,812 | 19,242 | 14,894 | 12,817 | 8,149 | 6,193 | 5,909 |
| 32 | 25,785 | 19,018 | 15,053 | 12,811 | 7,828 | 6,259 | 4,774 |
| 64 | 26,234 | 18,964 | 14,802 | 9,324 | 8,114 | 6,276 | 5,890 |
| 128 | 25,842 | 19,251 | 14,783 | 11,795 | 8,044 | 6,067 | 6,016 |
| 256 | 25,802 | 19,808 | 15,109 | 12,129 | 8,075 | 6,140 | 5,901 |

Eight php-fpm workers on four cores. The ceiling is already reached at eight connections; beyond that the requests queue in nginx. This table bounds the ceiling, it does not draw a saturation curve.

## How it was measured — and why it was restarted twice

This measurement was run three times. The first two were **abandoned**, both
found a real defect, and both are kept in the repository with the reasoning.

**First abort — the machine was not quiet.** Slim's `/json` gave 15,260 req/s in
one repetition and 2,107 in the next, with **p50 at 0.23 ms in both**. Identical
per-request cost, seven times fewer requests: the application had not slowed
down, the run had not been given CPU. The host is a laptop running its window
server, a terminal and an editor; cpusets isolate a container *inside* the VM,
they cannot isolate the VM from macOS.

The fix for that is not to take the average. Interference can only ever subtract
throughput — a run can be slowed by a noisy neighbour, but it cannot be faster
than the hardware allows. So the reported figure is the **best** of the
repetitions, with the median and the spread published beside it. A fixed nginx
reference that never reaches PHP is also sampled before and after every block:
across 150 samples the worst was 84% of the best.

**Second abort — port exhaustion.** Half the requests in the database scenarios
were returning 500: `SQLSTATE[HY000] [2002] Cannot assign requested address`.
php-fpm opens a fresh TCP connection to MySQL on every request and the container
runs out of ephemeral ports at a few thousand requests a second. The single
pre-flight request could never catch that. All seven moved to persistent
connections (`PDO::ATTR_PERSISTENT`, `pConnect` for CodeIgniter) — the documented
production setting for php-fpm. The effect is large: Slim's `/db` went from 3,594
to 14,074 req/s, which means the earlier measurement was measuring port churn
rather than the data layer.

> **Sınır**
>
> Three limits, stated plainly. **One:** the host could not be quiesced, and its
> slow regime caps throughput around 4-8k req/s, so it clips only the fast
> candidates — Laravel was clean in 9 of 9 repetitions, Slim in 3. The bias runs in
> one direction only: fast candidates are, if anything, **under**-reported.
> **Two:** oha counts the 64 connections it aborts at each deadline as requests, so
> `mean` and `max` are contaminated. The giveaway is arithmetic — `max × req/s`
> lands between 59 and 65 million for every candidate across three scenarios, so
> `max` is a function of throughput alone. Both are kept in the raw data and
> neither is quoted; the percentiles are unaffected. **Three:** the figures come
> from a Docker Desktop VM on Apple Silicon. Absolute values do not represent
> bare-metal production; comparison between candidates is valid, because they
> shared the same VM, the same cores and the same run.

## Findings that did not fit the table

1. **There is exactly one PHP version all seven agree on: 8.4.** Symfony 8.1
   requires at least `8.4.1`; `laminas/laminas-mvc` 3.8 accepts no more than
   `~8.4.0`. The window is one minor version wide.
2. **Phalcon and Symfony cannot share a PHP process.** Phalcon requires
   `ext-psr`, and with that extension loaded Symfony is fatal on PHP 8.4:
   `Declaration of Symfony\Component\Cache\CacheItem::expiresAt(?DateTimeInterface $expiration): static must be compatible with PsrExt\Cache\CacheItemInterface::expiresAt($expiration)`.
   The C extension declares the PSR interfaces without the parameter types the
   userland packages use. The "one image, one extension set" plan therefore forks
   in exactly one place: Phalcon runs on its own runtime image, the other six
   share one.
3. **CodeIgniter 4 has no PDO driver** — its MySQL support is MySQLi only.
   `ext-mysqli` is installed for all seven so the extension set stays identical;
   the other six never use it.
4. **Laminas's official skeleton has not been released for PHP 8.4.**
   `laminas/laminas-mvc-skeleton` 2.4.0 still declares
   `~8.1.0 || ~8.2.0 || ~8.3.0` and refuses to install without
   `--ignore-platform-req=php`. The component it boots,
   `laminas/laminas-mvc` 3.8.0, does accept `~8.4.0`.
5. **Yii2 is still 2.0.x.** Yii3 has no stable release, so the latest stable Yii
   is a line whose architecture predates the others' current versions.
6. **Middleware is not the same concept in all seven.** CodeIgniter filters and
   Yii2 action filters are scoped to a route and cost the other scenarios
   nothing. Symfony, Laminas and Phalcon have no per-route pipeline; the
   documented equivalent is a global event listener, so in those three all five
   layers are consulted on **every** request and return early. That is their
   architecture, not a handicap invented here.
7. **Slim does less work in two scenarios.** It has no data layer (`/db` runs on
   raw PDO) and no template engine of its own (`slim/php-view`). Its first place
   in those two rows should be read with that in mind.
8. **One cell contradicted itself and was topped up.** Slim's `/route` came out
   at 6,778 req/s over nine repetitions — with the lowest p50 in the field
   (0.23 ms), at 26% of its own empty-route throughput. None of the nine had
   landed in an unclipped window. Two rules fixed in advance (two or fewer clean
   repetitions, or throughput below half of `8/p50`) flagged six cells, all of
   them Slim or Yii2 — the fastest two, which is the direction the bias was
   predicted to run. Six repetitions were added to each; Slim's `/route` settled
   at 24,774 and no cell remained flagged.
9. **Zero non-200 responses.** After the persistent-connection fix, all 546 load
   measurements returned HTTP 200 for every request.
10. **JIT was off.** None of the seven documents opcache JIT as a production
    step, and enabling it would have added a second variable. Results with JIT on
    may differ; that is a separate measurement.
11. **Worker mode was not measured.** Persistent-process models such as
    FrankenPHP or Octane are not officially supported by all seven; classic
    php-fpm is the one floor they share. Worker mode deserves its own record.
