Roundhouse benchmark results

Run bench · captured 2026-06-23T04:00:10Z · a2c2eacde7db · 80 cells × 3 runs · 5 endpoints × 16 targets
Artifacts: env.json · per-run.json · summary.json · summary.md
Methodology. AMD Ryzen 5 3600 6-Core Processor, 6c/12t, 3.6 GHz, boost enabled, governor=schedutil, 63 GB RAM, Ubuntu 24.04.4 LTS. Toolchains pinned via mise. Harness: scripts/bench — workers=1, c=64, 3×20s runs per cell after 20s warmup; the median of the 3 runs is reported (the run-to-run spread is in the stability section below). Quiet machine throughout. The managed-heap cells run under a fixed heap budget so their RSS is comparable to the other cells' working sets rather than the runtime's default share of host RAM: the JVM cells (jruby, kotlin) via -Xmx512m=-Xms512m, and the .NET cells (csharp, csharp-aot) via the equivalent DOTNET_GCHeapHardLimit (512 MB). Full hardware, toolchain versions, and the exact invocation are in the environment appendix below.

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

rails-jruby is the stock-Rails-on-the-JVM baseline — the same blog, pinned to Rails 8.0 (one minor behind the others' 8.1) on a prerelease AR-JDBC adapter (activerecord-jdbcsqlite3-adapter 80.0.pre1), the only combination that drives ActiveRecord on JRuby 10 today.

spinel runs the same Roundhouse-emitted framework as the CRuby ruby cell, but on the Spinel VM behind the Tep fiber server rather than CRuby + Puma — so the rubyspinel gap is a runtime swap, not a framework or source change.

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/seckotlin56,400rust53,670csharp-aot43,367csharp40,362jruby28,325swift20,078crystal19,212spinel9,878go8,420elixir6,115python4,920ruby3,556ruby-int2,663rails-jruby1,128rails-int340rails333

/articles/1

1001,00010,000100,0001,000,000req/secrust77,323kotlin66,831csharp-aot54,333csharp53,709jruby36,711crystal25,428swift21,909elixir13,508spinel11,794go8,694python6,239ruby3,816ruby-int3,031rails-jruby1,057rails-int327rails325

/articles/new

1001,00010,000100,0001,000,000req/seckotlin138,021csharp-aot105,811rust105,618csharp95,088jruby52,647crystal51,283elixir46,242swift25,490spinel19,413go11,187python9,530ruby5,375ruby-int4,296rails-jruby1,757rails-int487rails475

/articles.json

1001,00010,000100,0001,000,000req/secrust85,188kotlin65,615csharp-aot54,931csharp54,555jruby38,552swift38,428crystal28,323go20,438elixir18,271spinel13,548python9,131ruby4,198ruby-int3,147rails-jruby894rails-int624rails609

/articles/1.json

1001,00010,000100,0001,000,000req/secrust153,777kotlin106,198csharp-aot104,096csharp94,022crystal52,764jruby52,173swift43,799go36,773elixir28,064spinel22,199python10,457ruby5,604ruby-int4,236rails-jruby1,202rails975rails-int956

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.

01,0002,0003,0004,0005,0006,0007,0003,556333/articles10.7×3,816325/articles/111.7×5,375475/articles/new11.3×4,198609/articles.json6.9×5,604975/articles/1.json5.7×rubyrails

The same comparison on the JVM: emitted jruby beats stock rails-jruby by 25× on /articles, against 11× for ruby over rails on CRuby.

3. YJIT contribution

Each pair compares a cell with YJIT (ruby, rails) against its interpreter-only variant (ruby-int, rails-int). The bars show how far YJIT moves each.

01,0002,0003,0004,0005,0006,0007,0003,5562,663333340/articles3,8163,031325327/articles/15,3754,296475487/articles/new4,1983,147609624/articles.json5,6044,236975956/articles/1.jsonrubyruby-intrailsrails-int

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

Throughput normalized by memory footprint — req/sec divided by RSS in GB. This reorders the raw throughput charts above: targets with small working sets rise and high-RSS targets fall. Bars are log-scaled. How much the metric matters depends on the deployment shape — most on metered or serverless surfaces, least on bare metal with memory headroom.

/articles

1,00010,000100,0001,000,00010,000,000req/sec/GBrust3,173,760crystal890,161spinel845,741csharp-aot610,204swift462,554csharp325,919go256,640kotlin96,332python62,339elixir46,433jruby30,879ruby28,972ruby-int26,587rails-int1,181rails1,079rails-jruby1,058

/articles/1

1,00010,000100,0001,000,00010,000,000req/sec/GBrust4,448,088spinel1,013,385crystal975,420csharp-aot740,639swift479,130csharp431,215go255,017elixir101,316kotlin95,467python78,770jruby37,795ruby29,055ruby-int25,475rails-int1,102rails1,027rails-jruby939

/articles/new

1,00010,000100,0001,000,00010,000,000req/sec/GBrust5,811,151crystal2,256,024spinel1,690,141csharp-aot1,420,962csharp761,764swift531,973elixir342,714go326,835kotlin184,806python120,307jruby53,993ruby36,206ruby-int33,526rails-int1,596rails-jruby1,496rails1,483

/articles.json

1,00010,000100,0001,000,00010,000,000req/sec/GBrust4,614,930crystal1,283,010spinel1,121,074swift780,365csharp-aot753,806go561,421csharp433,851elixir133,040python115,258kotlin78,221jruby38,987ruby27,629ruby-int24,051rails-int2,018rails1,885rails-jruby693

/articles/1.json

1,00010,000100,0001,000,00010,000,000req/sec/GBrust8,320,272crystal2,274,249spinel1,767,744csharp-aot1,409,150go1,010,490swift887,025csharp742,490elixir207,388python131,994kotlin123,653jruby52,759ruby37,043ruby-int31,263rails-int2,780rails2,735rails-jruby930

5. HTML → JSON multiplier

Per-target lift going from /articles (HTML) to /articles.json (JSON). The asymmetry isn't JSON-escape vs HTML-escape — both paths concatenate and escape strings. What JSON skips is the layout chain (csrf_meta_tag, importmap, asset-manifest scan, content_for slots) and the per-row view helpers (link_to, button_to, dom_id, tag.*) that the HTML index template invokes for every record. The chart below shows the multiplier per target.

rails-jruby's multiplier falls below 1× (0.8×): its JSON endpoint measures slower than its HTML one.

elixir3.0×go2.4×swift1.9×python1.9×rails1.8×rust1.6×crystal1.5×spinel1.4×jruby1.4×csharp1.4×csharp-aot1.3×ruby1.2×kotlin1.2×rails-jruby0.8×

6. Memory footprint (RSS)

Max RSS observed across all endpoints, per target, sorted low to high. The field spans more than two orders of magnitude. The managed-heap cells (the JVM and .NET targets) run under a fixed heap budget rather than an organically-grown working set — see the note below the chart.

spinel12 MBrust18 MBcrystal26 MBgo37 MBswift50 MBcsharp-aot76 MBpython81 MBcsharp129 MBruby-int138 MBelixir140 MBruby155 MBrails-int352 MBrails365 MBkotlin879 MBjruby1,012 MBrails-jruby1,322 MB

The JVM cells run under a fixed -Xmx512m heap, so the heap can't grab a host-dependent share of RAM; the rest is metaspace and code-cache, bounded by the app's class count rather than the box. Read the JVM bars as a pinned budget, not an organically-grown working set like the other cells.

The .NET cells are capped the same way — DOTNET_GCHeapHardLimit at 512 MB, the -Xmx analog — so their RSS is a budget the GC collects within, not the host-proportional reservation Server GC takes by default. NativeAOT (csharp-aot) carries no JIT or warmup, so its working set is the leanest of the managed-heap cells.

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

Latencies are at c=64 concurrent connections; treat p50 as the median per-connection wait and p99 as the tail. Rows are ordered by measured median p50 across endpoints, fastest first.

target/articles/articles/1/articles/new/articles.json/articles/1.json
p50p99p50p99p50p99p50p99p50p99
rust1.22.20.72.50.51.60.71.90.31.0
kotlin0.94.60.83.80.32.30.84.00.42.7
csharp1.34.71.04.00.43.41.04.20.53.2
csharp-aot1.34.71.04.30.42.81.03.40.52.5
jruby2.14.41.73.81.23.11.63.61.23.0
swift3.05.92.76.22.28.31.62.91.24.8
crystal3.33.82.43.21.21.72.22.91.21.6
go7.218.87.016.42.947.72.97.91.57.2
elixir10.315.84.68.41.24.03.46.82.24.4
spinel6.47.25.46.03.33.84.75.30.52.4
python12.913.610.110.86.67.37.07.46.06.7
ruby18.322.016.920.911.816.115.421.711.614.9
ruby-int24.228.621.426.115.019.820.527.015.024.2
rails-jruby52.775.055.881.232.752.969.093.050.073.0
rails-int181.4375.9188.6467.0127.3226.6102.2122.166.7155.8
rails186.1355.6190.4412.1130.9231.1105.3114.267.9192.9

All values in milliseconds. Lower is better.

Run-to-run stability

Each cell is timed 3 times and the charts above report the median run. This section reads per-run.json directly to show how far the individual runs strayed from it. Across all 80 cells the median run-to-run coefficient of variation in req/sec is 0.58% — the timed runs of a given cell agree to a fraction of a percent, so the reported medians aren't masking noise.

The 8 cells that vary by more than 3% are concentrated in the lowest-throughput cells, where a small absolute swing is a larger fraction of the rate; in 3 of them the first timed run is the slowest, consistent with residual warmup the fixed 20s warmup doesn't fully absorb:

targetendpointrun 1run 2run 3CV
rails-jruby/articles6381,1281,13624.0%
ruby/articles.json4,1983,7594,4406.8%
ruby-int/articles/new4,2964,4933,9415.4%
spinel/articles/1.json22,19924,28321,5965.1%
rails/articles/1.json8859879754.8%
ruby-int/articles2,6632,6512,8733.7%
rails-jruby/articles.json8338998943.4%
ruby-int/articles/1.json4,2364,4714,1693.0%

req/sec per timed run; CV = standard deviation ÷ mean.

Raw cell data (80 rows)
targetendpointreq/secp50 (ms)p99 (ms)RSS (MB)req/sec/GB
rust/articles53,6701.162.17173,173,760
rust/articles/177,3230.752.51174,448,088
rust/articles/new105,6180.461.57185,811,151
rust/articles.json85,1880.691.87184,614,930
rust/articles/1.json153,7770.341.01188,320,272
crystal/articles19,2123.303.8422890,161
crystal/articles/125,4282.413.2026975,420
crystal/articles/new51,2831.161.72232,256,024
crystal/articles.json28,3232.182.89221,283,010
crystal/articles/1.json52,7641.161.58232,274,249
go/articles8,4207.2018.7733256,640
go/articles/18,6947.0016.4134255,017
go/articles/new11,1872.8847.6635326,835
go/articles.json20,4382.897.8837561,421
go/articles/1.json36,7731.537.22371,010,490
csharp-aot/articles43,3671.274.6872610,204
csharp-aot/articles/154,3331.014.3275740,639
csharp-aot/articles/new105,8110.432.84761,420,962
csharp-aot/articles.json54,9311.013.4174753,806
csharp-aot/articles/1.json104,0960.452.53751,409,150
kotlin/articles56,4000.924.5659996,332
kotlin/articles/166,8310.773.8171695,467
kotlin/articles/new138,0210.342.34764184,806
kotlin/articles.json65,6150.794.0385878,221
kotlin/articles/1.json106,1980.442.70879123,653
swift/articles20,0782.985.8944462,554
swift/articles/121,9092.666.1846479,130
swift/articles/new25,4902.158.3149531,973
swift/articles.json38,4281.562.9150780,365
swift/articles/1.json43,7991.154.7950887,025
csharp/articles40,3621.264.69126325,919
csharp/articles/153,7090.974.05127431,215
csharp/articles/new95,0880.443.36127761,764
csharp/articles.json54,5550.984.22128433,851
csharp/articles/1.json94,0220.483.17129742,490
spinel/articles9,8786.417.1611845,741
spinel/articles/111,7945.356.05111,013,385
spinel/articles/new19,4133.273.80111,690,141
spinel/articles.json13,5484.695.34121,121,074
spinel/articles/1.json22,1990.512.35121,767,744
elixir/articles6,11510.3315.8113446,433
elixir/articles/113,5084.648.44136101,316
elixir/articles/new46,2421.224.03138342,714
elixir/articles.json18,2713.436.76140133,040
elixir/articles/1.json28,0642.204.41138207,388
python/articles4,92012.9013.638062,339
python/articles/16,23910.0810.858178,770
python/articles/new9,5306.607.2681120,307
python/articles.json9,1316.997.3781115,258
python/articles/1.json10,4576.036.6681131,994
ruby/articles3,55618.3021.9912528,972
ruby/articles/13,81616.9320.8913429,055
ruby/articles/new5,37511.7816.1115236,206
ruby/articles.json4,19815.4521.6715527,629
ruby/articles/1.json5,60411.6514.8615437,043
ruby-int/articles2,66324.2428.5910226,587
ruby-int/articles/13,03121.3926.1012125,475
ruby-int/articles/new4,29614.9819.7513133,526
ruby-int/articles.json3,14720.5427.0413424,051
ruby-int/articles/1.json4,23614.9524.2313831,263
jruby/articles28,3252.134.4293930,879
jruby/articles/136,7111.663.8499437,795
jruby/articles/new52,6471.173.1099853,993
jruby/articles.json38,5521.593.601,01238,987
jruby/articles/1.json52,1731.172.951,01252,759
rails/articles333186.11355.603161,079
rails/articles/1325190.36412.093241,027
rails/articles/new475130.87231.123281,483
rails/articles.json609105.30114.193301,885
rails/articles/1.json97567.86192.943652,735
rails-int/articles340181.35375.902941,181
rails-int/articles/1327188.56467.003041,102
rails-int/articles/new487127.28226.623121,596
rails-int/articles.json624102.25122.143162,018
rails-int/articles/1.json95666.72155.823522,780
rails-jruby/articles1,12852.6775.001,0911,058
rails-jruby/articles/11,05755.7981.171,152939
rails-jruby/articles/new1,75732.7352.931,2011,496
rails-jruby/articles.json89469.0293.041,320693
rails-jruby/articles/1.json1,20249.9972.991,322930
Environment (captured 2026-06-23T04:00:10Z)

Host

hostnameshowcase.party
OSUbuntu 24.04.4 LTS
kernelLinux showcase.party 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 13:00:45 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
boardASRock B450 Pro4 R2.0
CPUAMD Ryzen 5 3600 6-Core Processor
topology6 cores / 12 threads
clockgovernor=schedutil, boost enabled, 3600 MHz max
memory64,221 MB

Toolchains

cargocargo 1.95.0 (f2d3ce0bd 2026-03-21)
crystalCrystal 1.20.2 [2482c62c1] (2026-05-15)
curlcurl 8.5.0 (x86_64-pc-linux-gnu) libcurl/8.5.0 OpenSSL/3.0.13 zlib/1.3 brotli/1.1.0 zstd/1.5.5 libidn2/2.3.7 libpsl/0.21.2 (+libidn2/2.3.7) libssh/0.10.6/openssl/zlib nghttp2/1.59.0 librtmp/2.3 OpenLDAP/2.6.10
dotnet10.0.301
gogo version go1.26.4 linux/amd64
jrubyjruby 10.1.0.0 (4.0.0) 2026-04-20 32f988b78c OpenJDK 64-Bit Server VM 25.0.3+9-LTS on 25.0.3+9-LTS +indy +jit [x86_64-linux]
mise2026.5.15 linux-x64 (2026-05-23)
nodev26.3.0
python3Python 3.14.6
rubyruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
rustcrustc 1.95.0 (59807616e 2026-04-14)
shardsShards 0.20.0 [b2b98ca] (2025-12-19)
sqlite33.45.1 2024-01-30 16:01:20 e876e51a0ed5c5b3126f52e532044363a014bc594cfefa87ffb5b82257ccalt1 (64-bit)
uvuv 0.11.22 (x86_64-unknown-linux-musl)
wrkwrk debian/4.1.0-4build2 [epoll] Copyright (C) 2012 Will Glozer

Harness

commandscripts/bench --port 19000 rails rails-int rails-jruby ruby ruby-int jruby python spinel typescript elixir go csharp csharp-aot swift kotlin crystal rust
workers1
concurrency64
runs3 × 20s after 20s warmup
wrk threads2
endpoints/articles /articles/1 /articles/new /articles.json /articles/1.json
targetsrails, rails-int, rails-jruby, ruby, ruby-int, jruby, python, spinel, typescript, elixir, go, csharp, csharp-aot, swift, kotlin, crystal, rust
not measuredtypescript (requested but produced no cells)

Source

commita2c2eacde7dbb8c7a50eb5e30b57c7d529f5c620
branchmain
subjectanalyze: Int rounding, to_* fallback on Var recv, flatten/dedup unions (#57 Tier 1)

Conditions at start

load average0.24 / 0.08 / 0.02
uptime06:00:10 up 11 days, 12:46, 1 user, load average: 0.24, 0.08, 0.02

Generated 2026-06-24T01:51:00Z from summary.json.