Roundhouse benchmark results

Run bench · captured 2026-08-05T04:00:15Z · ef9d815d02f8 · 85 cells × 3 runs · 5 endpoints × 17 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,167rust53,084csharp-aot43,857csharp40,758jruby23,103crystal19,806swift19,590typescript11,845spinel9,469go8,491elixir6,124python4,784ruby3,309ruby-int2,235rails-jruby1,140rails-int335rails329

/articles/1

1001,00010,000100,0001,000,000req/secrust78,591kotlin66,371csharp-aot55,280csharp54,766jruby29,232crystal25,987swift21,731elixir13,626typescript13,498go8,803python6,249ruby3,405ruby-int2,507rails-jruby1,058rails-int325rails322spinel0

/articles/new

1001,00010,000100,0001,000,000req/seckotlin138,811csharp-aot107,297rust103,743csharp96,995crystal52,496elixir46,372jruby40,239typescript26,803swift26,686go11,394python9,665ruby4,529ruby-int3,271rails-jruby1,727rails-int472rails466spinel0

/articles.json

1001,00010,000100,0001,000,000req/secrust86,756kotlin65,581csharp55,241csharp-aot55,121swift38,963jruby32,962crystal28,931go20,725elixir18,090typescript13,373python8,810ruby4,028ruby-int3,156rails-jruby883rails600rails-int595spinel0

/articles/1.json

1001,00010,000100,0001,000,000req/secrust154,797csharp-aot107,652kotlin106,159csharp95,916crystal55,061jruby46,824swift45,198go36,620elixir28,833typescript21,109python10,485ruby5,824ruby-int4,298rails-jruby1,168rails-int949rails890spinel0

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,309329/articles10.0×3,405322/articles/110.6×4,529466/articles/new9.7×4,028600/articles.json6.7×5,824890/articles/1.json6.5×rubyrails

The same comparison on the JVM: emitted jruby beats stock rails-jruby by 20× on /articles, against 10× 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,3092,235329335/articles3,4052,507322325/articles/14,5293,271466472/articles/new4,0283,156600595/articles.json5,8244,298890949/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,073,961crystal719,548csharp-aot614,620spinel505,801swift446,448csharp331,693go257,976kotlin107,388python61,149elixir51,858typescript39,206jruby27,081ruby25,504ruby-int18,881rails-int1,159rails1,094rails-jruby1,082

/articles/1

1,00010,000100,0001,000,00010,000,000req/sec/GBrust4,463,238crystal1,070,486csharp-aot750,429swift483,141csharp408,879go256,108elixir112,223kotlin100,716python79,637typescript44,502jruby29,809ruby24,665ruby-int20,020rails-int1,077rails1,038rails-jruby928spinel0

/articles/new

1,00010,000100,0001,000,00010,000,000req/sec/GBrust5,610,865crystal2,391,262csharp-aot1,430,396csharp721,103swift590,613elixir380,032go329,969kotlin195,310python123,176typescript88,462jruby40,662ruby30,123ruby-int24,493rails-int1,528rails-jruby1,502rails1,483spinel0

/articles.json

1,00010,000100,0001,000,00010,000,000req/sec/GBrust4,704,726crystal1,337,847swift859,687csharp-aot739,313go569,251csharp409,458elixir142,000python112,279kotlin82,487typescript45,053jruby33,070ruby25,752ruby-int22,767rails-int1,916rails1,887rails-jruby753spinel0

/articles/1.json

1,00010,000100,0001,000,00010,000,000req/sec/GBrust8,375,460crystal2,536,722csharp-aot1,442,612go1,005,531swift998,025csharp713,062elixir231,935python133,615kotlin130,138typescript71,497jruby46,975ruby33,211ruby-int26,687rails-int2,606rails2,562rails-jruby996spinel0

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×swift2.0×python1.8×rails1.8×rust1.6×crystal1.5×jruby1.4×csharp1.4×csharp-aot1.3×ruby1.2×kotlin1.2×typescript1.1×rails-jruby0.8×spinel0.0×

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.

rust18 MBspinel19 MBcrystal28 MBgo37 MBswift46 MBcsharp-aot76 MBpython80 MBelixir130 MBcsharp138 MBruby-int164 MBruby179 MBtypescript310 MBrails355 MBrails-int373 MBkotlin835 MBjruby1,020 MBrails-jruby1,201 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.80.31.0
kotlin0.94.40.83.50.32.20.83.80.42.6
csharp1.24.71.03.90.43.31.04.10.52.9
csharp-aot1.33.81.03.80.42.21.03.20.42.1
jruby2.65.02.14.41.53.71.94.01.33.3
crystal3.34.02.43.11.11.72.12.91.11.5
swift3.16.12.76.12.25.51.52.81.42.3
go7.218.86.916.12.946.42.97.71.57.2
elixir10.315.84.68.41.24.03.56.92.14.4
typescript5.210.34.58.82.34.44.58.72.85.4
spinel6.77.60.00.00.00.00.00.00.00.0
python13.313.910.210.96.66.97.27.86.06.7
ruby19.326.919.024.614.419.016.021.611.115.5
ruby-int29.035.426.032.520.025.521.128.815.219.9
rails-jruby51.778.155.990.433.353.670.0102.051.975.0
rails-int185.0354.7191.4458.4132.0226.2108.0128.066.9192.2
rails188.7354.7192.6516.9133.0232.6106.5126.069.8162.4

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 81 cells the median run-to-run coefficient of variation in req/sec is 0.56% — the timed runs of a given cell agree to a fraction of a percent, so the reported medians aren't masking noise.

The 12 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 4 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/articles7201,1401,14519.9%
rails/articles/1.json1,05882289010.7%
ruby/articles3,3093,8313,0279.8%
ruby-int/articles2,1172,2352,6239.3%
rails-int/articles/1.json1,0779499007.6%
ruby-int/articles.json3,2163,1562,8006.0%
ruby-int/articles/1.json4,2984,1874,7655.7%
rails-jruby/articles/19641,0581,0654.5%
ruby/articles/new4,5824,5294,1494.4%
ruby/articles/1.json6,0935,8245,5423.9%
ruby/articles.json3,9504,0284,2803.4%
ruby-int/articles/new3,4353,1773,2713.2%

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

Raw cell data (85 rows)
targetendpointreq/secp50 (ms)p99 (ms)RSS (MB)req/sec/GB
rust/articles53,0841.182.20173,073,961
rust/articles/178,5910.722.46184,463,238
rust/articles/new103,7430.471.59185,610,865
rust/articles.json86,7560.671.81184,704,726
rust/articles/1.json154,7970.340.97188,375,460
crystal/articles19,8063.273.9628719,548
crystal/articles/125,9872.353.11241,070,486
crystal/articles/new52,4961.131.68222,391,262
crystal/articles.json28,9312.142.88221,337,847
crystal/articles/1.json55,0611.121.51222,536,722
go/articles8,4917.1518.7733257,976
go/articles/18,8036.9316.1435256,108
go/articles/new11,3942.8746.4435329,969
go/articles.json20,7252.867.7437569,251
go/articles/1.json36,6201.547.17371,005,531
csharp-aot/articles43,8571.273.7873614,620
csharp-aot/articles/155,2801.003.8175750,429
csharp-aot/articles/new107,2970.422.24761,430,396
csharp-aot/articles.json55,1211.003.1576739,313
csharp-aot/articles/1.json107,6520.442.08761,442,612
kotlin/articles56,1670.924.40535107,388
kotlin/articles/166,3710.783.53674100,716
kotlin/articles/new138,8110.342.21727195,310
kotlin/articles.json65,5810.793.7781482,487
kotlin/articles/1.json106,1590.442.63835130,138
swift/articles19,5903.066.0944446,448
swift/articles/121,7312.686.1146483,141
swift/articles/new26,6862.215.5046590,613
swift/articles.json38,9631.542.7746859,687
swift/articles/1.json45,1981.412.3446998,025
csharp/articles40,7581.254.67125331,693
csharp/articles/154,7660.953.94137408,879
csharp/articles/new96,9950.433.34137721,103
csharp/articles.json55,2410.974.07138409,458
csharp/articles/1.json95,9160.462.88137713,062
spinel/articles9,4696.717.6319505,801
spinel/articles/100.000.0000
spinel/articles/new00.000.0000
spinel/articles.json00.000.0000
spinel/articles/1.json00.000.0000
elixir/articles6,12410.3115.8112051,858
elixir/articles/113,6264.618.36124112,223
elixir/articles/new46,3721.224.00124380,032
elixir/articles.json18,0903.476.86130142,000
elixir/articles/1.json28,8332.134.41127231,935
typescript/articles11,8455.2110.3230939,206
typescript/articles/113,4984.488.7931044,502
typescript/articles/new26,8032.294.4331088,462
typescript/articles.json13,3734.488.7330345,053
typescript/articles/1.json21,1092.845.4430271,497
python/articles4,78413.3413.898061,149
python/articles/16,24910.1710.888079,637
python/articles/new9,6656.606.8780123,176
python/articles.json8,8107.237.7680112,279
python/articles/1.json10,4856.036.6780133,615
ruby/articles3,30919.3226.9013225,504
ruby/articles/13,40519.0024.6514124,665
ruby/articles/new4,52914.3819.0115330,123
ruby/articles.json4,02816.0521.5816025,752
ruby/articles/1.json5,82411.1215.4617933,211
ruby-int/articles2,23528.9835.4212118,881
ruby-int/articles/12,50726.0432.4612820,020
ruby-int/articles/new3,27120.0225.4813624,493
ruby-int/articles.json3,15621.1328.8214122,767
ruby-int/articles/1.json4,29815.1819.9116426,687
jruby/articles23,1032.655.0087327,081
jruby/articles/129,2322.094.371,00429,809
jruby/articles/new40,2391.523.691,01340,662
jruby/articles.json32,9621.863.981,02033,070
jruby/articles/1.json46,8241.293.301,02046,975
rails/articles329188.66354.743081,094
rails/articles/1322192.60516.873171,038
rails/articles/new466133.01232.553211,483
rails/articles.json600106.50125.973251,887
rails/articles/1.json89069.78162.423552,562
rails-int/articles335185.03354.722961,159
rails-int/articles/1325191.38458.403091,077
rails-int/articles/new472131.96226.253161,528
rails-int/articles.json595107.98127.983171,916
rails-int/articles/1.json94966.86192.193732,606
rails-jruby/articles1,14051.7078.111,0791,082
rails-jruby/articles/11,05855.8690.391,167928
rails-jruby/articles/new1,72733.2753.611,1771,502
rails-jruby/articles.json88370.04102.021,199753
rails-jruby/articles/1.json1,16851.9475.011,201996
Environment (captured 2026-08-05T04:00:15Z)

Host

hostnameshowcase.party
OSUbuntu 24.04.4 LTS
kernelLinux showcase.party 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:53:05 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,228 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

Source

commitef9d815d02f8baf2dd5dbf78e306d8568e885184
branchmain
subjectbench(lobsters): REPRODUCE.md carries the #3513 crash recipe, not just the benchmark

Conditions at start

load average0.86 / 0.21 / 0.06
uptime06:00:15 up 2 days, 20:30, 4 users, load average: 0.86, 0.21, 0.06

Generated 2026-08-05T20:11:28Z from summary.json.