# Ruuter Benchmark Results Each benchmark: 2s warmup, 5s measurement window. ## Environment - **Machine**: Macbook Pro (M4 Max 48GB) - **JVM**: Java 25.0.2, Clojure 1.12.4 - **ClojureScript**: 1.12.134, Node.js v24.11.1 - **Babashka**: 1.12.212 - **Jank**: 0.1 (Apr 2026) ## JVM (Clojure) ### v1.3.5 — first-match-wins, linear scan with per-request regex | Scenario | Ops/sec | ns/op | |---|---:|---:| | Small (5) — literal first | 2,361,346 | 423 | | Small (5) — literal middle | 1,372,498 | 728 | | Small (5) — param match | 1,093,600 | 914 | | Small (5) — nested params | 706,667 | 1,414 | | Small (5) — wildcard | 1,112,373 | 898 | | Small (5) — miss (404) | 2,363,419 | 423 | | Medium (52) — match first | 196,908 | 5,078 | | Medium (52) — match middle | 47,393 | 21,100 | | Medium (52) — match last | 19,720 | 50,710 | | Medium (52) — catch-all wildcard | 17,999 | 55,558 | | Large (202) — match first | 49,959 | 20,016 | | Large (202) — match middle | 10,780 | 92,764 | | Large (202) — match last | 5,090 | 196,464 | | Large (202) — miss (404) | 10,262 | 97,447 | ### v2.0.0 — best-match-wins, segment trie with specificity scoring | Scenario | Ops/sec | ns/op | |---|---:|---:| | Small (5) — literal first | 9,788,503 | 102 | | Small (5) — literal middle | 4,704,213 | 212 | | Small (5) — param match | 2,296,556 | 435 | | Small (5) — nested params | 1,650,276 | 605 | | Small (5) — wildcard | 1,833,884 | 545 | | Small (5) — miss (404) | 5,957,701 | 167 | | Medium (52) — match first | 8,757,566 | 114 | | Medium (52) — match middle | 1,836,957 | 544 | | Medium (52) — match last | 1,812,725 | 551 | | Medium (52) — catch-all wildcard | 2,504,034 | 399 | | Large (202) — match first | 8,090,398 | 123 | | Large (202) — match middle | 1,764,621 | 566 | | Large (202) — match last | 1,683,626 | 593 | | Large (202) — miss (404) | 3,541,419 | 282 | ### JVM Comparison | Scenario | v1.3.5 ops/s | v2.0.0 ops/s | Speedup | |---|---:|---:|---:| | Small (5) — literal first | 2,361,346 | 9,788,503 | 4.1x | | Small (5) — literal middle | 1,372,498 | 4,704,213 | 3.4x | | Small (5) — param match | 1,093,600 | 2,296,556 | 2.1x | | Small (5) — nested params | 706,667 | 1,650,276 | 2.3x | | Small (5) — wildcard | 1,112,373 | 1,833,884 | 1.6x | | Small (5) — miss (404) | 2,363,419 | 5,957,701 | 2.5x | | Medium (52) — match first | 196,908 | 8,757,566 | 44x | | Medium (52) — match middle | 47,393 | 1,836,957 | 39x | | Medium (52) — match last | 19,720 | 1,812,725 | 92x | | Medium (52) — catch-all wildcard | 17,999 | 2,504,034 | 139x | | Large (202) — match first | 49,959 | 8,090,398 | 162x | | Large (202) — match middle | 10,780 | 1,764,621 | 164x | | Large (202) — match last | 5,090 | 1,683,626 | 331x | | Large (202) — miss (404) | 10,262 | 3,541,419 | 345x | ## ClojureScript (Node.js) ### v1.3.5 | Scenario | Ops/sec | ns/op | |---|---:|---:| | Small (5) — literal first | 1,403,030 | 712 | | Small (5) — literal middle | 543,057 | 1,841 | | Small (5) — param match | 179,074 | 5,584 | | Small (5) — nested params | 169,373 | 5,904 | | Small (5) — wildcard | 167,200 | 5,980 | | Small (5) — miss (404) | 170,835 | 5,853 | | Medium (52) — match first | 27,378 | 36,524 | | Medium (52) — match middle | 25,216 | 39,656 | | Medium (52) — match last | 15,788 | 63,335 | | Medium (52) — catch-all wildcard | 16,028 | 62,389 | | Large (202) — match first | 27,263 | 36,678 | | Large (202) — match middle | 6,449 | 155,043 | | Large (202) — match last | 4,090 | 244,489 | | Large (202) — miss (404) | 4,028 | 248,201 | ### v2.0.0 | Scenario | Ops/sec | ns/op | |---|---:|---:| | Small (5) — literal first | 1,273,187 | 785 | | Small (5) — literal middle | 849,002 | 1,177 | | Small (5) — param match | 468,241 | 2,135 | | Small (5) — nested params | 335,675 | 2,979 | | Small (5) — wildcard | 370,342 | 2,700 | | Small (5) — miss (404) | 1,104,045 | 905 | | Medium (52) — match first | 1,100,616 | 908 | | Medium (52) — match middle | 361,869 | 2,763 | | Medium (52) — match last | 358,480 | 2,789 | | Medium (52) — catch-all wildcard | 506,195 | 1,975 | | Large (202) — match first | 1,028,507 | 972 | | Large (202) — match middle | 349,154 | 2,864 | | Large (202) — match last | 345,575 | 2,893 | | Large (202) — miss (404) | 671,639 | 1,488 | ### ClojureScript Comparison | Scenario | v1.3.5 ops/s | v2.0.0 ops/s | Speedup | |---|---:|---:|---:| | Small (5) — literal first | 1,403,030 | 1,273,187 | 0.9x | | Small (5) — literal middle | 543,057 | 849,002 | 1.6x | | Small (5) — param match | 179,074 | 468,241 | 2.6x | | Small (5) — nested params | 169,373 | 335,675 | 2.0x | | Small (5) — wildcard | 167,200 | 370,342 | 2.2x | | Small (5) — miss (404) | 170,835 | 1,104,045 | 6.5x | | Medium (52) — match first | 27,378 | 1,100,616 | 40x | | Medium (52) — match middle | 25,216 | 361,869 | 14x | | Medium (52) — match last | 15,788 | 358,480 | 23x | | Medium (52) — catch-all wildcard | 16,028 | 506,195 | 32x | | Large (202) — match first | 27,263 | 1,028,507 | 38x | | Large (202) — match middle | 6,449 | 349,154 | 54x | | Large (202) — match last | 4,090 | 345,575 | 84x | | Large (202) — miss (404) | 4,028 | 671,639 | 167x | ## Babashka ### v1.3.5 | Scenario | Ops/sec | ns/op | |---|---:|---:| | Small (5) — literal first | 178,061 | 5,616 | | Small (5) — literal middle | 178,653 | 5,597 | | Small (5) — param match | 125,608 | 7,961 | | Small (5) — nested params | 121,220 | 8,249 | | Small (5) — wildcard | 121,145 | 8,254 | | Small (5) — miss (404) | 172,150 | 5,808 | | Medium (52) — match first | 27,824 | 35,939 | | Medium (52) — match middle | 25,177 | 39,718 | | Medium (52) — match last | 15,973 | 62,604 | | Medium (52) — catch-all wildcard | 16,087 | 62,161 | | Large (202) — match first | 27,853 | 35,901 | | Large (202) — match middle | 6,631 | 150,790 | | Large (202) — match last | 4,157 | 240,542 | | Large (202) — miss (404) | 4,206 | 237,747 | ### v2.0.0 | Scenario | Ops/sec | ns/op | |---|---:|---:| | Small (5) — literal first | 842,584 | 1,186 | | Small (5) — literal middle | 540,559 | 1,849 | | Small (5) — param match | 321,034 | 3,114 | | Small (5) — nested params | 241,899 | 4,133 | | Small (5) — wildcard | 324,357 | 3,083 | | Small (5) — miss (404) | 1,107,631 | 902 | | Medium (52) — match first | 903,709 | 1,106 | | Medium (52) — match middle | 285,129 | 3,507 | | Medium (52) — match last | 292,190 | 3,422 | | Medium (52) — catch-all wildcard | 424,338 | 2,356 | | Large (202) — match first | 888,860 | 1,125 | | Large (202) — match middle | 290,947 | 3,437 | | Large (202) — match last | 290,664 | 3,440 | | Large (202) — miss (404) | 767,125 | 1,303 | ### Babashka Comparison | Scenario | v1.3.5 ops/s | v2.0.0 ops/s | Speedup | |---|---:|---:|---:| | Small (5) — literal first | 178,061 | 842,584 | 4.7x | | Small (5) — literal middle | 178,653 | 540,559 | 3.0x | | Small (5) — param match | 125,608 | 321,034 | 2.6x | | Small (5) — nested params | 121,220 | 241,899 | 2.0x | | Small (5) — wildcard | 121,145 | 324,357 | 2.7x | | Small (5) — miss (404) | 172,150 | 1,107,631 | 6.4x | | Medium (52) — match first | 27,824 | 903,709 | 32x | | Medium (52) — match middle | 25,177 | 285,129 | 11x | | Medium (52) — match last | 15,973 | 292,190 | 18x | | Medium (52) — catch-all wildcard | 16,087 | 424,338 | 26x | | Large (202) — match first | 27,853 | 888,860 | 32x | | Large (202) — match middle | 6,631 | 290,947 | 44x | | Large (202) — match last | 4,157 | 290,664 | 70x | | Large (202) — miss (404) | 4,206 | 767,125 | 182x | ## Jank ### v2.1.0 | Scenario | Ops/sec | ns/op | |---|---:|---:| | Small (5) — literal first | 962,079 | 1,039 | | Small (5) — literal middle | 442,877 | 2,257 | | Small (5) — param match | 223,006 | 4,484 | | Small (5) — nested params | 146,768 | 6,813 | | Small (5) — wildcard | 182,129 | 5,490 | | Small (5) — miss (404) | 655,948 | 1,524 | | Medium (52) — match first | 899,823 | 1,111 | | Medium (52) — match middle | 181,990 | 5,494 | | Medium (52) — match last | 181,771 | 5,501 | | Medium (52) — catch-all wildcard | 272,411 | 3,670 | | Large (202) — match first | 875,741 | 1,141 | | Large (202) — match middle | 178,075 | 5,615 | | Large (202) — match last | 176,088 | 5,678 | | Large (202) — miss (404) | 482,470 | 2,072 | ## Analysis ### JVM (Clojure) - **Small route sets**: 1.6–4.1x faster - **Medium route sets**: 39–139x faster - **Large route sets**: 162–345x faster - Peak throughput: ~9.8M ops/sec (literal match) ### ClojureScript (Node.js) - **Small route sets**: 0.9–6.5x faster (literal-first is within noise; params, wildcards, and misses see large gains) - **Medium route sets**: 14–40x faster - **Large route sets**: 38–167x faster - Peak throughput: ~1.3M ops/sec (literal match) ### Babashka - **Small route sets**: 2.0–6.4x faster - **Medium route sets**: 11–32x faster - **Large route sets**: 32–182x faster - Peak throughput: ~1.1M ops/sec (miss/404 — fast trie rejection) ### Jank - Peak throughput: ~962K ops/sec (literal match) - Performance is now close to Babashka peak throughput on this benchmark suite ### Key insight v2.0 performance is nearly independent of route count across all runtimes. Matching is O(path depth) via the trie, not O(route count) via linear scan. The improvement is most dramatic for large route sets where v1.x's linear scan with per-request regex compilation becomes the dominant bottleneck. ### Running benchmarks ```bash # JVM clojure -Sdeps '{:paths ["src" "bench"]}' -M -m ruuter.bench # ClojureScript (Node.js) clojure -Sdeps '{:paths ["src" "bench"] :deps {org.clojure/clojurescript {:mvn/version "1.10.879"}}}' \ -M -m cljs.main --target node --output-to bench-out/bench.js -c ruuter.bench node bench-out/bench.js # Babashka bb -m ruuter.bench # Jank jank run --module-path src:bench jank_bench_runner.jank ``` ### Route set sizes - **Small (5)**: 5 routes — typical small app (literal, params, wildcard) - **Medium (52)**: 52 routes — 1 literal + 50 parameterized + 1 catch-all wildcard - **Large (202)**: 202 routes — 1 literal + 200 parameterized + 1 catch-all wildcard