Roundhouse benchmark results

Run 20260526-002218 · 40 cells × 3 runs · 5 endpoints × 8 targets
Methodology. Hetzner AX41-NVMe (Ryzen 5 3600, 6c/12t, 3.6 GHz base, boost disabled, governor=performance), 64 GB ECC DDR4, NVMe SSD, Ubuntu 24.04. Toolchains via mise — Ruby 4.0.2+YJIT, Rust 1.94, Go 1.24, Node 23, Crystal 1.16. Harness: scripts/bench defaults — workers=1, c=64, 3×20s runs per cell. Quiet machine throughout (Kamal services moved off bench port).

These numbers measure one specific Rails reference app — not arbitrary Rails workloads. See #16 for the fixture-fragility caveats.

1. Throughput across targets

Each endpoint is its own chart. Bars are log-scaled; raw req/sec is shown at the right.

/articles

1001,00010,000100,0001,000,000req/secrust37,109crystal14,308typescript5,072ruby3,835go3,344ruby-int2,985rails-int489rails477

/articles/1

1001,00010,000100,0001,000,000req/secrust53,860crystal17,238typescript6,771ruby4,272go3,713ruby-int3,578rails-int476rails468

/articles/new

1001,00010,000100,0001,000,000req/secrust91,887crystal39,144typescript18,352go10,692ruby6,658ruby-int5,381rails-int693rails682

/articles.json

1001,00010,000100,0001,000,000req/secrust98,161crystal34,846go13,770typescript10,665ruby5,659ruby-int4,609rails-int909rails857

/articles/1.json

1001,00010,000100,0001,000,000req/secrust126,002crystal47,837go14,175typescript12,366ruby6,692ruby-int5,318rails-int1,314rails1,265

2. Lowerer dividend (ruby vs rails)

Same Ruby interpreter, same YJIT, same Puma — the only variable is whether the framework runtime is Rails or Roundhouse-emitted. The multiplier above each pair is the lift from the lowerer pipeline.

02,0004,0006,0008,0003,835477/articles8.0×4,272468/articles/19.1×6,658682/articles/new9.8×5,659857/articles.json6.6×6,6921,265/articles/1.json5.3×rubyrails

3. YJIT contribution

YJIT consistently helps Roundhouse-emitted Ruby (small, predictable call shapes). On Rails it's neutral-to-slightly-negative — the deep autoload + reflection chain blunts it.

02,0004,0006,0008,0003,8352,985477489/articles4,2723,578468476/articles/16,6585,381682693/articles/new5,6594,609857909/articles.json6,6925,3181,2651,314/articles/1.jsonrubyruby-intrailsrails-int

4. Cost economics (req/sec per GB of RSS)

Throughput normalized by memory footprint. Reorders the table — rust and crystal pull further ahead; high-RSS targets (typescript, rails) move down. Applicability varies by deployment shape: matters most for metered/serverless surfaces, least for bare-metal-with-headroom.

/articles

1,00010,000100,0001,000,00010,000,000req/sec/GBrust2,202,902crystal626,412go104,237ruby38,179ruby-int34,978typescript13,157rails-int1,633rails1,535

/articles/1

1,00010,000100,0001,000,00010,000,000req/sec/GBrust3,128,583crystal843,232go114,172ruby33,971ruby-int31,853typescript17,570rails-int1,549rails1,441

/articles/new

1,00010,000100,0001,000,00010,000,000req/sec/GBrust5,198,063crystal2,055,590go308,971ruby48,756typescript47,845ruby-int44,183rails-int2,159rails2,052

/articles.json

1,00010,000100,0001,000,00010,000,000req/sec/GBrust5,529,102crystal1,552,464go415,116ruby40,430ruby-int36,948typescript27,793rails-int2,748rails2,521

/articles/1.json

1,00010,000100,0001,000,00010,000,000req/sec/GBrust7,089,683crystal2,108,329go428,220ruby46,887ruby-int40,863typescript32,195rails-int3,479rails3,256

5. HTML → JSON multiplier

Per-target lift going from /articles (HTML) to /articles.json (JSON). Compiled targets gain the most because the JSON path skips the view-render pipeline (string concat, HTML escape, layout wrap). The Go gap motivates the IR string-builder annotation work in #18.

rust2.6×crystal2.4×go4.1×typescript2.1×ruby1.5×rails1.8×

6. Memory footprint (RSS)

Max RSS observed across all endpoints, per target. Three orders of magnitude separate rust from rails on the same workload.

rust18 MBcrystal23 MBgo35 MBruby-int133 MBruby146 MBrails-int386 MBtypescript394 MBrails397 MB

7. Latency (p50 / p99 at c=64)

Latencies are c=64 concurrent connections; treat p50 as median per-connection wait, p99 as tail. For the AOT/JIT targets these are sub-millisecond on the no-DB endpoint.

target/articles/articles/1/articles/new/articles.json/articles/1.json
p50p99p50p99p50p99p50p99p50p99
rust1.73.21.22.20.61.60.51.60.51.3
crystal4.45.13.64.51.61.91.72.21.31.7
go19.124.117.221.23.537.44.67.24.57.0
typescript12.024.19.115.23.44.25.711.94.813.1
ruby16.618.714.823.09.511.711.114.19.512.0
ruby-int21.423.217.821.911.814.113.816.512.014.2
rails128.0301.0131.9269.691.0218.375.398.748.174.2
rails-int125.3312.6128.9212.089.6223.370.087.246.254.4

All values in milliseconds. Lower is better.

Raw cell data (40 rows)
targetendpointreq/secp50 (ms)p99 (ms)RSS (MB)req/sec/GB
rust/articles37,1091.703.16172,202,902
rust/articles/153,8601.152.25173,128,583
rust/articles/new91,8870.621.55185,198,063
rust/articles.json98,1610.531.64185,529,102
rust/articles/1.json126,0020.461.29187,089,683
crystal/articles14,3084.425.0923626,412
crystal/articles/117,2383.644.4720843,232
crystal/articles/new39,1441.581.94192,055,590
crystal/articles.json34,8461.742.22221,552,464
crystal/articles/1.json47,8371.291.72232,108,329
go/articles3,34419.1024.0732104,237
go/articles/13,71317.2321.2533114,172
go/articles/new10,6923.4937.4235308,971
go/articles.json13,7704.587.1833415,116
go/articles/1.json14,1754.497.0233428,220
typescript/articles5,07211.9624.1139413,157
typescript/articles/16,7719.0615.1839417,570
typescript/articles/new18,3523.434.1839247,845
typescript/articles.json10,6655.7111.9439227,793
typescript/articles/1.json12,3664.8413.1239332,195
ruby/articles3,83516.6518.6810238,179
ruby/articles/14,27214.7522.9812833,971
ruby/articles/new6,6589.5011.7313948,756
ruby/articles.json5,65911.1214.0914340,430
ruby/articles/1.json6,6929.4611.9514646,887
ruby-int/articles2,98521.4423.248734,978
ruby-int/articles/13,57817.7721.9311531,853
ruby-int/articles/new5,38111.8114.0912444,183
ruby-int/articles.json4,60913.8016.4612736,948
ruby-int/articles/1.json5,31812.0214.2013340,863
rails/articles477128.00301.003181,535
rails/articles/1468131.93269.633321,441
rails/articles/new68291.04218.323402,052
rails/articles.json85775.3498.673482,521
rails/articles/1.json1,26548.0974.243973,256
rails-int/articles489125.32312.623061,633
rails-int/articles/1476128.91212.033141,549
rails-int/articles/new69389.60223.303282,159
rails-int/articles.json90970.0487.183392,748
rails-int/articles/1.json1,31446.2354.413863,479

Generated 2026-05-26T00:18:17Z from summary.json.