ruta

Router benchmark

Re-runnable benchmark comparing ruta against nikic/fast-route and symfony/routing under various configurations.

Running it

The benchmark is a standalone composer project so the comparison libraries never become dependencies of ruta itself (ruta is pulled in through a path repository):

cd benchmark
composer install
php run.php

It can also be run on demand from GitHub via the manually triggered Benchmark workflow (Actions → Benchmark → Run workflow), which prints the result tables to the job summary.

For numbers closer to a production web request, enable opcache for the CLI run:

php -d opcache.enable_cli=1 run.php

Options

--routes=60,600,1200   comma separated route-set sizes
--shapes=rest,static   route-set shapes to benchmark
--subjects=...         ruta, ruta-cached, fastroute, fastroute-cached,
                       symfony, symfony-compiled (default: all)
--iterations=20000     dispatches per measurement
--repeats=7            measurements per cell (median reported)
--warmup=2000          warmup dispatches before measuring

What is measured

Route sets are generated deterministically, so every router and every run sees exactly the same routes and requests. Two shapes are generated:

Subjects (one column block per scenario, values are the median over the repeats):

subject what it is
ruta Ruta\Router, routes registered in memory
ruta (cached) Ruta\cachedRouter() loading the route map from its cache file
fastroute FastRoute\simpleDispatcher (GroupCountBased)
fastroute (cached) FastRoute\cachedDispatcher loading from its cache file
symfony Symfony\Component\Routing\Matcher\UrlMatcher over a RouteCollection
symfony (compiled) CompiledUrlMatcher loading a dumped, compiled matcher from a file

Metrics:

Fairness notes:

Results

Run on PHP 8.4.19 (CLI, opcache off), Linux container, defaults (20000 iterations × 7 repeats, medians). Re-run locally for numbers on your hardware; relative ordering is what matters.

Shape rest — 60 routes (20000 iterations × 7 repeats, median)

router setup µs static (hit) µs/op dynamic (hit) µs/op dynamic deep (hit) µs/op not found µs/op
ruta 65.8 1.17 1.55 1.64 1.13
ruta (cached) 35.4 1.16 1.64 1.64 1.18
fastroute 133 0.21 0.80 0.84 1.23
fastroute (cached) 55.8 0.22 0.82 0.86 1.27
symfony 79.5 6.88 8.26 9.21 11.8
symfony (compiled) 139 0.58 1.10 1.13 1.29

Shape rest — 600 routes (20000 iterations × 7 repeats, median)

router setup µs static (hit) µs/op dynamic (hit) µs/op dynamic deep (hit) µs/op not found µs/op
ruta 660 1.84 2.29 2.40 2.37
ruta (cached) 269 1.67 2.15 2.19 2.28
fastroute 2,698 0.23 2.45 2.48 7.03
fastroute (cached) 530 0.24 2.45 2.49 7.15
symfony 883 53.7 55.0 55.8 99.3
symfony (compiled) 1,409 0.59 1.38 1.46 1.59

Shape rest — 1200 routes (20000 iterations × 7 repeats, median)

router setup µs static (hit) µs/op dynamic (hit) µs/op dynamic deep (hit) µs/op not found µs/op
ruta 1,399 3.16 3.59 3.62 3.91
ruta (cached) 564 2.32 2.81 2.94 3.63
fastroute 8,526 0.22 4.31 4.35 14.0
fastroute (cached) 1,144 0.23 4.27 4.30 13.7
symfony 1,809 105 106 108 197
symfony (compiled) 2,970 0.57 1.69 1.69 1.88

Shape static — 60 routes (20000 iterations × 7 repeats, median)

router setup µs static (hit) µs/op not found µs/op
ruta 57.5 1.20 1.06
ruta (cached) 25.3 1.17 1.06
fastroute 58.9 0.22 0.27
fastroute (cached) 24.5 0.23 0.28
symfony 77.8 7.22 11.0
symfony (compiled) 128 0.59 1.12

Shape static — 600 routes (20000 iterations × 7 repeats, median)

router setup µs static (hit) µs/op not found µs/op
ruta 589 1.47 1.09
ruta (cached) 135 1.36 1.06
fastroute 638 0.21 0.27
fastroute (cached) 151 0.22 0.28
symfony 925 54.3 99.3
symfony (compiled) 1,490 0.60 1.19

Shape static — 1200 routes (20000 iterations × 7 repeats, median)

router setup µs static (hit) µs/op not found µs/op
ruta 1,255 1.88 1.10
ruta (cached) 278 1.50 1.06
fastroute 1,353 0.21 0.27
fastroute (cached) 315 0.22 0.27
symfony 1,860 111 202
symfony (compiled) 3,039 0.60 1.13

Interpretation