Skip to content

Performance ​

Version 4 compiles the dynamic routes of each method into one regular expression shaped as an order-preserving prefix tree. With 1,000 routes, lookups took 0.1–0.6 µs in every measured layout except optional prefixes (3–7 µs), compared with 4.6–191 µs in v3.5. Building a router costs 1.7–2 times as much as in v3.5, but with RouteCache a request loads a prebuilt router in under 2 µs, independent of the number of routes.

These are routing microbenchmarks, not full-application requests per second. They include neither HTTP serving nor controller, middleware, database, or network work.

Lookup with 1,000 routes ​

Microseconds per lookup, median of seven runs of 10,000 lookups. Each table has 1,000 GET routes: a resource endpoint and the same endpoint with an ID, interleaved. Requests cycle through all routes.

LayoutRequestv3.5v4Leaf 5Symfony compiledSlim 4
/resourceN, /resourceN/{id}static0.190.130.170.270.24
dynamic4.700.430.540.572.07
last route8.630.420.470.544.30
40416.820.310.330.677.53
4058.680.44—4.392.28
/api/resourceN/{id}dynamic4.600.4517.260.592.30
40416.940.3339.170.679.01
/api/{tenant}/resourceN/{id}dynamic48.420.54—0.736.79
404183.770.40—0.7729.08
/{lang:[a-z]{2}}/resourceN/{id}dynamic43.700.53—0.737.61
last route87.010.51—0.6516.94
404167.150.42—0.8333.61
40580.280.54—7.588.39
[/{lang:[a-z]{2}}]/resourceN/{id}dynamic49.123.20———
last route95.166.78———
404190.816.10———
/resourceN/{id:\d+}dynamic4.650.44—0.592.08

“Static” requests the resource endpoints; in the tenant and locale layouts they are dynamic too. “404” appends /missing; “405” sends POST to GET routes. HEAD requests falling back to GET cost about the same as dynamic hits. A dash means the library does not support that layout or request with the measured API.

Lookup with 100 routes ​

LayoutRequestv3.5v4Symfony compiledSlim 4
/resourceN/{id}dynamic0.760.310.450.56
4041.870.210.521.10
/{lang:[a-z]{2}}/resourceN/{id}dynamic3.420.340.490.77
40411.780.210.551.78
[/{lang:[a-z]{2}}]/resourceN/{id}dynamic3.780.53——

Optional segments ​

Routes share prefixes only where sharing cannot change which route matches first. An optional segment can be present or absent, so backtracking into a shared one could reach a later route before an earlier one. Routes that start with an optional segment are therefore listed one by one inside the pattern. PCRE still rejects them quickly (3–7 µs with 1,000 such routes, against 49–191 µs in v3.5), but lookup time grows with their number. The same applies to parameters whose regex can match /, such as {path:.*}.

Building ​

Milliseconds to register the routes, build the router and resolve one request, median in a warm process. Without a route cache, a PHP-FPM application pays this on every request. “v4 cached” loads a warm RouteCache file instead and resolves one request.

LayoutRoutesv3.5v4v4 cachedLeaf 5Symfony compiledSlim 4
/resourceN/{id}1000.070.110.00140.120.590.17
1,0000.631.180.00161.139.5610.18
/api/{tenant}/resourceN/{id}1000.110.200.0015—0.990.14
1,0001.162.290.0018—11.341.46
/{lang:[a-z]{2}}/resourceN/{id}1000.150.260.0015—0.990.14
1,0001.492.840.0018—11.341.51

Building validates every pattern and builds the prefix tree; PHP caches the compiled regex itself across requests in the same worker. A cached router skips both: OPcache serves the tables from shared memory, and Route objects are created only for the routes a request uses. Symfony and Laravel applications normally cache their compiled matchers too; their uncached numbers above are not what they pay in production. Clear the cache whenever routes change.

Measurement method and limits ​

Measurements were recorded on 25 October 2026 with PHP 8.4.21 CLI in an ARM64 Linux container on Apple Silicon. OPcache was enabled and PHP JIT disabled; PCRE 10.44 ran with its JIT; Xdebug was absent.

  • Libraries: Solo Router v3.5.0 and this version, Leaf 5.0.1, Symfony Routing 7.4.21 (CompiledUrlMatcher), and Slim 4.15.3 with FastRoute 1.3.1. v3.5 resolves with match() followed by getAllowedMethods() on a miss, the work needed to tell 404 from 405.
  • All requests were prepared before timing. Every result, including route names and parameters, was checked for every library before the timed runs, which also warmed routes and PCRE caches. The run order of the libraries rotated.
  • Leaf was measured only for GET requests in layouts with a literal first segment; Symfony and FastRoute do not support optional segments before other segments.
  • This is one machine and a synthetic workload, without confidence intervals or randomized request distributions. Minor differences should not be treated as stable advantages.

The scripts, the full tables for every layout and request type, and the JSON reports are in benchmarks/ in the repository.

Released under the MIT License.