# Research, Benchmarks & Analyses

> Measurements, analyses, ecosystem surveys and source reviews: which question I chased, how I went at it, what I found and how far I trust the result.

- Record count: 11
- Source: https://muhammetsafak.com/research/
- Language: en-US
- Author: Muhammet Şafak

---
## All programmes

- **Language & runtime** (4) — What version upgrades, compiler and runtime settings change on the same workload: JIT, opcache, GC and memory behaviour. — https://muhammetsafak.com/research/program/dil-runtime/
- **Database & queries** (3) — What index strategy, query plans and connection handling cost once the data volume is real — schema and migration decisions included. — https://muhammetsafak.com/research/program/veritabani/
- **Service & load** (2) — Latency distribution, saturation point and queue behaviour under load — p95/p99, not the mean; backpressure and circuit breakers included. — https://muhammetsafak.com/research/program/servis-yuk/
- **Tooling comparison** (1) — The measurable difference between two tools doing the same job: build time, output size, memory and the developer loop. — https://muhammetsafak.com/research/program/arac-karsilastirma/
- **Frontend performance** (1) — Shipped JS, first paint and interaction delay: how many kilobytes a convenience costs the visitor, browser support included. — https://muhammetsafak.com/research/program/arayuz-performans/
- **Cost & resources** (0) — The side of a decision that reaches the invoice: CPU hours, bandwidth, storage and true cost per request. — https://muhammetsafak.com/research/program/maliyet/

## Findings ledger

### 64 goroutines on four cores: Mutex and channel run at the same speed, but the Mutex's p99 is five times higher

- Kind: Measurement
- Question: When G goroutines on P cores hammer the same shared counter, how many operations a second do sync.Mutex, an owner-goroutine channel and a buffered channel manage, and how do tail latency and fairness of waiting change?
- Finding: With 64 goroutines on four cores, sync.Mutex and a one-way channel run at the same speed (7.69 and 7.94 million operations/s) but the Mutex's p99 wait is 5 times higher. On eight cores the Mutex is 2.4 and 4.0 times faster (15.96 million operations/s; the channels 6.65 and 3.97 million); the ratios come from unflagged cells.
- Measured on: 2026-09-29
- Confidence: Low confidence
- Programme: dil-runtime
- Raw data: https://github.com/muhammetsafak/go-sync-bench/tree/main/results/2026-09-29
- https://muhammetsafak.com/research/mutex-vs-channel-under-contention/
- Markdown: https://muhammetsafak.com/research/mutex-vs-channel-under-contention.md

### One core carries 14,330 OAuth2 requests in Go and 5,152 in PHP-FPM

- Kind: Measurement
- Question: The same API verifies an RS256 bearer token on every request and then reads or writes one row in PostgreSQL — on one, two and four cores, how much mixed traffic does it carry in Go, PHP-FPM and FrankenPHP worker mode?
- Finding: On four cores Go carried 57,321 mixed requests a second, FrankenPHP 25,659 and php-fpm 20,606 — 14,330, 6,415 and 5,152 per core. The number that goes into a capacity plan is not that one but application CPU per request: 66.8, 110.7 and 187.7 microseconds. At saturation FrankenPHP uses only 2.84 of its four cores, against Go's 3.83 and php-fpm's 3.87. The database is not the constraint: on the same four cores PostgreSQL alone writes 68,212 rows a second, above the mixed ceiling of the fastest candidate.
- Measured on: 2026-09-17
- Confidence: Medium confidence
- Programme: servis-yuk
- Raw data: https://github.com/muhammetsafak/php-go-bench/tree/main/results/capacity-2026-09-17
- https://muhammetsafak.com/research/capacity-per-core-go-php-oauth2/
- Markdown: https://muhammetsafak.com/research/capacity-per-core-go-php-oauth2.md

### Laravel's preload curve: 123 files buy eight times what the last 1,912 do

- Kind: Measurement
- Question: How far can a curated preload take Laravel, and what does each slice cost in start-up time?
- Finding: The curve is not proportional to volume. The first 1,592 files — Laravel's own framework — buy 30 ms and add 1.2 seconds to start-up. The next 1,094 Symfony files buy 9.5 ms for free. The **123 files** after that (psr, carbon) buy 15.7 ms, more than the 1,094 before them. And the last 1,912 buy 1.8 ms while adding another 1.2 seconds. So the blanket preload the earlier record measured as a ceiling is the worst point on the curve that is not the origin: stopping at 2,809 files gives 12.77 ms for 1,514 ms of start-up, while 4,721 files ask 2,691 ms to reach 10.96 ms.
- Measured on: 2026-08-22
- Confidence: High confidence
- Programme: dil-runtime
- Raw data: https://github.com/muhammetsafak/php-framework-bench/blob/main/results/2026-08-22/preload-curate.json
- https://muhammetsafak.com/research/laravel-preload-curve/
- Markdown: https://muhammetsafak.com/research/laravel-preload-curve.md

### A partial index makes a queue table forty-one times smaller — for as long as the planner picks it

- Kind: Measurement
- Question: On a Postgres queue table with millions of dead rows, what does a partial index buy, and when does the planner refuse to use it?
- Finding: At 10 million dead rows a partial index sustains 11,537 claims per second where the same table without one manages 7. Against a composite index the throughput difference is small (6.9%) but the size difference is not: 7.6 MB against 310.4 MB, and the partial one does not grow with the table because it indexes only the 5,000 live rows. None of that is the real finding. The moment the planner switches a prepared statement to a generic plan the partial index stops being used at all — 11,752 tps becomes 7, and 0.68 ms becomes 1.1 seconds. A factor of 1,635. The composite index is untouched under the same conditions.
- Measured on: 2026-08-22
- Confidence: High confidence
- Programme: veritabani
- Raw data: https://github.com/muhammetsafak/pg-queue-bench/blob/main/results/2026-08-22/queue.json
- https://muhammetsafak.com/research/partial-index-queue-table/
- Markdown: https://muhammetsafak.com/research/partial-index-queue-table.md

### The partial index grew three hundred and five times in fifteen minutes — and autovacuum never ran

- Kind: Measurement
- Question: Under sustained churn, does a partial index stay small on a queue table, and do the default autovacuum settings keep up with it?
- Finding: With the live set holding at five thousand rows until about 845 seconds, the partial index went from 0.125 MB to 38.2 MB — three hundred and five times. Its smallness comes from the live set, its bloat rate comes from throughput, and nothing connects the two. The composite index bloated less in proportion (42%) and more in absolute terms (+126 MB), and while bloating it fell over — most likely because it no longer fit in memory, a cause this run did not measure: its latency went from 0.52 ms to 61 seconds and its backlog climbed to 126,000. Fifteen minutes produced 1.75 million dead rows and autovacuum **did not run once** — the default threshold scales with the whole table (50 + 0.2 × 10 million ≈ 2 million) while the churn happens in a tiny subset.
- Measured on: 2026-08-22
- Confidence: High confidence
- Programme: veritabani
- Raw data: https://github.com/muhammetsafak/pg-queue-bench/blob/main/results/2026-08-22/endurance-partial.jsonl
- https://muhammetsafak.com/research/queue-table-bloat-and-autovacuum/
- Markdown: https://muhammetsafak.com/research/queue-table-bloat-and-autovacuum.md

### Postgres never turned the partial index into a generic plan: forty executions, forty custom plans

- Kind: Measurement
- Question: Does Postgres switch a partial-index query to a generic plan on its own inside a prepared statement — or is the 1,635-fold cliff something you have to opt into?
- Finding: Postgres declines. On the partial index all forty executions used a custom plan — the counter reads 40/0. The reason it declines is the disaster itself: a generic plan cannot use the partial index, so its estimated cost comes out high and the planner does not choose it. The composite index switches at the sixth execution exactly as documented (5/35) and loses nothing by it. So the 1,635-fold cliff is real but fenced: reaching it takes writing `plan_cache_mode = force_generic_plan`.
- Measured on: 2026-08-22
- Confidence: High confidence
- Programme: veritabani
- Raw data: https://github.com/muhammetsafak/pg-queue-bench/blob/main/results/2026-08-22/plan-switch.json
- https://muhammetsafak.com/research/when-does-the-planner-go-generic/
- Markdown: https://muhammetsafak.com/research/when-does-the-planner-go-generic.md

### How you import Chart.js decides how many kilobytes the visitor downloads

- Kind: Measurement
- Question: In a real production build, what is the difference between `chart.js/auto` and a selective `Chart.register()` — in kilobytes?
- Finding: Selective registration saves 9.7 kB gzip over `chart.js/auto` (67.8 → 58.1 kB, 14.3%). The larger drop is not in the library core but in leaving unused controllers out: a page that registers only the bar chart falls to 46.0 kB — two thirds of auto.
- Measured on: 2026-08-19
- Confidence: High confidence
- Programme: arayuz-performans
- https://muhammetsafak.com/research/chartjs-import-strategy/
- Markdown: https://muhammetsafak.com/research/chartjs-import-strategy.md

### opcache preload cuts the deploy bill by up to fourteen times — but five of seven frameworks do not hand it to you

- Kind: Measurement
- Question: With `opcache.preload` on, how long is the first request seven PHP frameworks serve after a deploy, what does the gain cost, and who can actually have it?
- Finding: Preload shortens the cold first request by between 3.5× and 14.2× on the six candidates whose classes are PHP source: Symfony drops from 35.58 ms to 2.50 ms, down to Phalcon's bare figure. But only two of the seven candidates — Symfony and CodeIgniter — publish a preload file of their own; for the other five the gain sits on the table waiting for the user to write one. Writing one is not as easy as it looks: a preload generated blindly from the classmap never brings Symfony up at all, and on CodeIgniter it does worse (5.29 ms) than the hand-picked official file (3.13 ms). And the cost does not vanish: Laravel's classmap preload takes the 62 ms it saves each visitor and writes it back as 2,340 ms of php-fpm start-up.
- Measured on: 2026-08-22
- Confidence: High confidence
- Programme: dil-runtime
- Raw data: https://github.com/muhammetsafak/php-framework-bench/blob/main/results/2026-08-22/preload.json
- https://muhammetsafak.com/research/opcache-preload-deploy-bill/
- Markdown: https://muhammetsafak.com/research/opcache-preload-deploy-bill.md

### The PHP ecosystem does not wait for a new release — but it does not declare support either

- Kind: Survey
- Question: After a PHP release ships, when do the 500 most-installed Composer packages declare that they support it, and how much does that declaration actually say?
- Finding: 56.4% of installs arrive on a constraint with no upper bound at all — `symfony/console` says `>=8.4.1` today, which claims support for PHP 12 as well. Of the 280 packages that do close the top, 247 made the commitment before the version existed: `guzzlehttp/guzzle` covered 8.4 in October 2020, four years early. That leaves 33 packages that genuinely waited, at a median of 325 days. The raw medians fall version over version and read as an ecosystem speeding up; restricted to an equal observation window the trend reverses (238 → 215 → 325 days).
- Checked on: 2026-08-22
- Confidence: High confidence
- Programme: dil-runtime
- Raw data: https://github.com/muhammetsafak/packagist-php-support/blob/main/results/2026-08-22/php-support.json
- https://muhammetsafak.com/research/php-version-support-declarations/
- Markdown: https://muhammetsafak.com/research/php-version-support-declarations.md

### The fixed cost of installing a PHP framework: disk size tells you nothing

- Kind: Measurement
- Question: How many megabytes on disk, how many files per request and how many milliseconds on the first request do seven PHP frameworks cost — and which of those numbers actually predicts throughput under load?
- Finding: Disk size predicts nothing: Yii2 has the largest vendor tree at 34.1 MB and loads only 62 files per request, among the fewest in the field. Files per request predicts nothing either: CodeIgniter loads 96 files and serves 6,431 req/s, Symfony loads 224 and serves 13,067. The one number that genuinely separates them is the first request served with a cold opcache: 2.2 ms for Phalcon, 72.2 ms for Laravel — thirty-three times. That is the compile bill the first visitor pays after every deploy, and it is measured in tens of milliseconds, not kilobytes.
- Measured on: 2026-08-20
- Confidence: High confidence
- Programme: arac-karsilastirma
- Raw data: https://github.com/muhammetsafak/php-framework-bench/blob/main/results/2026-08-20/static.json
- https://muhammetsafak.com/research/php-framework-footprint/
- Markdown: https://muhammetsafak.com/research/php-framework-footprint.md

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

- 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.
- Measured on: 2026-08-20
- Confidence: Medium confidence
- Programme: servis-yuk
- Raw data: https://github.com/muhammetsafak/php-framework-bench/tree/main/results/2026-08-20
- https://muhammetsafak.com/research/php-framework-load-test/
- Markdown: https://muhammetsafak.com/research/php-framework-load-test.md
