Lobsters benchmark results

Run lobsters · 2026-07-30T15:18:23Z · a09ad5b9c09a · spinel 83f1b90397a1

The performance numbers on this page are not trustworthy

Timings below are still shown, and are still real measurements of what the code did — but what the code did was not what Rails does, so they do not measure the same work and must not be compared. This banner is generated from the run's own data and disappears by itself once every lane renders what Rails renders and every timed visit returns 200.

26/26 routes return 200 3.69× faster than Rails spinel 25/26 routes
Artifacts: matrix.json · env.json · rails/rails.json · rails-int/rails.json · ruby/summary.json · ruby-int/summary.json · spinel/summary.json
Methodology. The route table replays each distinct lobsters route once against the Roundhouse emit and records status, response bytes, and single-shot latency. The stability and comparison numbers replay a frozen 114-visit sequence (versioned at bench/lobsters/frozen-sequence.json) so every run — and the Rails baseline — walks the identical path. Both stacks serve from an in-memory SQLite seeded at boot (file:lobsters_bench?mode=memory&cache=shared), the shape the ruby-bench lobsters benchmark runs. Median of the timed iterations is reported.

The runtime × JIT matrix section re-runs the same frozen sequence once per interpreter × JIT lane — in-process, in-memory, single-threaded — so its rows differ only in the runtime under test.

Every lane runs ANALYZE once after seeding its in-memory copy (a departure from stock ruby-bench, whose fixture DB carries no sqlite_stat1): without planner statistics SQLite misplans the hottest-stories SELECT under /rss, /hottest, and /recent — a full-table walk plus sort instead of reading hotness_idx to the LIMIT, ~100× the query cost — and both stacks pay that same planner accident. Statistics restore the plan a production database would have; both lanes get identical stats, so the comparison is unchanged in kind and fairer in degree.

This measures one specific reference app (the ruby-bench lobsters fixture), not arbitrary Rails workloads. The Rails baseline runs the real Rails lobsters over the same sequence; the roundhouse cell serves the same routes from the Roundhouse-emitted framework.

1. Roundhouse vs Rails — runtime × JIT

3.69× faster than Rails — 133.6 ms/iter vs Rails' 493.2, both stacks CRuby with YJIT. The full field:

Every lane replays the same frozen sequence in-process, in-memory, single-threaded — no HTTP server, no load generator — so the only variable across rows is the interpreter × JIT under test (the ruby-bench lobsters-for-YJIT shape). Lower is better; whiskers span each lane's min–max.

Roundhouse AOT101.6 ms4.85× vs Railsn=99Roundhouse YJIT133.6 ms3.69× vs Railsn=76Roundhouse no JIT177.2 ms4.67× vs Railsn=57Rails YJIT493.2 msn=21Rails no JIT827.0 msn=20
stackJITms/itermin–maxCVdriftitersvs Rails
RoundhouseAOT101.6099.0–105.01.6%+4.2%994.85×
RoundhouseYJIT133.64118.6–148.47.0%+10.4%763.69×
Roundhouseno JIT177.22167.7–189.73.0%-7.2%574.67×
RailsYJIT493.18481.2–516.41.6%-1.0%211.00×
Railsno JIT827.04817.2–850.40.9%-0.0%201.00×

Drifting lanes: Roundhouse YJIT slowed 10.4% between the first and last quarter of its own timed iterations; Roundhouse no JIT sped up 7.2% between the first and last quarter of its own timed iterations. A drifting lane's median depends on how long the run was, so its ratio is not a stable number — read it as a range, not a point. Lanes that hold steady drift under 1%.

YJIT during the timed window
lanecode regioniseqs compiledinvalidationscode GCscompile time
Roundhouse YJIT3,187 KiB / 4,704 KiB+5+10+19.9 ms

Deltas across the timed iterations only — warmup is excluded, so a zero row means YJIT had finished its work before the clock started. Non-zero iseqs compiled means new code was still being generated while being measured; invalidations means compiled code was being thrown away and redone; a non-zero code GCs means the code region filled and YJIT reclaimed it, after which previously-compiled paths must be compiled again.

“vs Rails” is a speed ratio — Rails' ms/iter ÷ this row's, at the same JIT level — so 3× means three times faster. Memory is a separate section below. CV = per-lane standard deviation ÷ mean across the timed iterations.

Per-lane iteration stability (5 lanes)

Roundhouse AOT — 101.6 ms/iter median over 99 iterations (5 warmup)

99102105iteration 1iteration 99

Roundhouse YJIT — 133.6 ms/iter median over 76 iterations (15 warmup)

119134148iteration 1iteration 76

Roundhouse no JIT — 177.2 ms/iter median over 57 iterations (15 warmup)

168177190iteration 1iteration 57

Rails YJIT — 493.2 ms/iter median over 21 iterations (15 warmup)

481493516iteration 1iteration 21

Rails no JIT — 827.0 ms/iter median over 20 iterations (15 warmup)

817827850iteration 1iteration 20

Each column is one timed iteration, scaled within a tight band around the lane's spread; the dashed line is the median.

Lanes measured 2026-07-30T15:38:56Z · host showcase.party — one process per lane, JIT pinned per lane.

2. Per-endpoint breakdown

Where the overall 3.69× win comes from — each endpoint's median latency over every timed visit of the frozen sequence, per lane. Bars are log-scaled; the ratio at the right is Rails ÷ roundhouse (both CRuby +YJIT) (green = roundhouse faster). The chart draws rails, roundhouse, roundhouse AOT; the no-JIT lanes appear as extra columns in the table below.

A diagnostic breakdown, not a re-derivation: per-endpoint medians don't sum to the sequence median (GC and ordering don't decompose linearly), and the sub-millisecond rows sit near clock resolution — trust the ranking, not the third decimal.

railsroundhouseroundhouse AOT0110100ms/threads4.1×/newest4.3×/threads/:username4.0×/hottest2.7×/rss2.1×/comments3.0×/s/:story_id5.4×/top?length=1w1.7×/top?length=1d1.7×/top?length=1y1.7×/saved8.9×/u/:username4.9×/settings12.2×/recent11.6×/u4.9×/replies/comments10.1×/replies/stories9.8×/upvoted/comments7.9×/active9.5×/hidden9.9×/upvoted/stories9.7×/replies/unread9.4×/replies7.9×/comments/:comment_id/reply6.2×/about5.3×
Per-endpoint table (25 endpoints)
endpointrailsrails no-JITroundhouseroundhouse no-JITAOTratiovisits/iter
/threads12.8222.483.134.880.604.10×3
/newest12.6022.882.904.382.344.35×5
/threads/:username9.9417.132.503.980.733.97×2
/hottest9.8218.453.686.082.302.67×2
/rss9.3814.214.545.702.612.07×4
/comments7.5713.712.563.673.512.96×4
/s/:story_id5.599.991.021.640.835.45×15
/top?length=1w4.005.472.382.542.701.68×2
/top?length=1d3.955.402.362.532.681.67×4
/top?length=1y3.765.202.192.322.541.72×2
/saved3.365.990.380.600.268.88×5
/u/:username3.165.230.650.950.424.88×6
/settings2.864.880.230.361.0612.24×3
/recent2.634.430.230.370.1711.57×2
/u2.163.100.440.500.414.94×15
/replies/comments1.882.810.190.280.2210.08×3
/replies/stories1.852.800.190.280.179.84×2
/upvoted/comments1.843.170.230.340.227.88×3
/active1.833.390.190.310.149.54×6
/hidden1.803.290.180.300.179.92×4
/upvoted/stories1.793.160.180.300.149.73×6
/replies/unread1.792.910.190.290.119.36×7
/replies1.782.750.220.330.227.91×3
/comments/:comment_id/reply1.682.840.270.410.316.20×5
/about1.101.890.200.310.485.35×1

All lane columns are median ms per visit; ratio = rails ÷ roundhouse (the +YJIT pair). The AOT column is the spinel-compiled binary — same emit, different runtime, and not at full content parity (see above).

3. Memory footprint

Peak resident memory of each lane's replay process — lower is better. Because the lanes replay in-process, each one measures itself (VmHWM from /proc/self/status), so this is the whole stack: interpreter, framework, the in-memory fixture DB, and whatever the timed run retains on top.

The bar splits at the point the sequence starts. The solid segment is the baseline — everything loaded and seeded, before a single visit. The translucent segment is growth across the timed run. The two say different things: baseline is what the stack costs to stand up, growth is what serving retains.

Roundhouse AOT72 MB51 MB baseline + 21 MB growthRoundhouse no JIT102 MB85 MB baseline + 17 MB growthRoundhouse YJIT122 MB87 MB baseline + 36 MB growthRails no JIT305 MB175 MB baseline + 131 MB growthRails YJIT344 MB183 MB baseline + 161 MB growth
stackJITbaselinepeakgrowthvs Rails (memory)
RoundhouseAOT51 MB72 MB+21 MB
Roundhouseno JIT85 MB102 MB+17 MB3.00×
RoundhouseYJIT87 MB122 MB+36 MB2.81×
Railsno JIT175 MB305 MB+131 MB1.00×
RailsYJIT183 MB344 MB+161 MB1.00×

“vs Rails (memory)” is peak RSS ÷ peak RSS at the same JIT level — not the speed ratio in the matrix table above; the two differ. Baseline is sampled after the in-memory fixture DB is seeded, and it is not a common floor across lanes — each stack loads its own framework, so subtracting one lane's baseline from another's peak is meaningless.

4. Coverage & parity (26/26 · spinel 25/26)

Incomplete coverage: 1 route(s) fail on the spinel AOT lane — the failing routes' visits short-circuit cheaply, so treat the affected lane's timing rows above as optimistic. Details in the table.

Per-route table (26 routes)
routestatusbytesmsAOTAOT bytesAOT ms
/u200292,44365.14200292,44310.99
/active2002,4977.662002,4970.34
/newest20064,14960.0220064,3012.61
/recent2002,72413.362002,7240.35
/hottest20016,36951.1620063,9444.72
/rss20016,94220.7320063,9441.90
/privacy2002,4090.712002,4090.12
/about2002,2820.312002,2820.23
/settings20015,2191.6320015,2190.36
/top?length=1d2002,6263.992002,6262.87
/top?length=1w2002,6294.702002,6292.56
/top?length=1y2002,6292.422002,6292.59
/hidden2002,5522.332002,5520.17
/saved2005,20716.132005,1970.59
/upvoted/stories2002,7252.802002,7250.16
/comments20036,32758.2120036,1273.29
/upvoted/comments2002,7780.702002,7780.32
/threads20060,63239.422002,5750.63
/comments/6srqij/reply2002,60412.122002,6040.30
/threads/michell_wiegand20060,6418.932002,5840.73
/u/michell_wiegand2004,2632.325001170.39
/replies2002,7240.812002,7240.25
/replies/comments2002,7200.432002,7200.24
/replies/stories2002,7270.372002,7270.25
/replies/unread2002,7641.212002,7640.53
/s/enr7ye2007,69222.692005,8870.72

ms columns are single-shot diagnostic latencies — use the medians above for performance. An AOT byte count is flagged when it falls more than 10% short of the CRuby lane's — a 200 that renders a fraction of the page.

Why they fail — grouped by reason, because a shared cause is one fix:

laneroute(s)reason
AOT/u/michell_wiegandNotImplementedError: gem facade: Markdowner.to_html is stubbed (write-path only; see runtime/gem_facades.rb)

5. Environment & provenance

Run, stacks, sequence

Run

commandscripts/bench-lobsters rails rails-int ruby ruby-int spinel --time 20 --src /home/rubys/ruby-bench/benchmarks/lobsters
source/home/rubys/ruby-bench/benchmarks/lobsters
commita09ad5b9c09a3a932c8c68333be8f86f7e356fec
fixture commit09b76c8b0f94c8182080cdae01e95151f0e22dd9
captured2026-07-30T15:38:56Z
databasefile:lobsters_bench?mode=memory&cache=shared
story / commentenr7ye / 6srqij

Stacks

roundhouse rubyruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [x86_64-linux]
rails rubyruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [x86_64-linux]
rails8.1.1

AOT toolchain

spinel (AOT)spinel 83f1b90397a1
C compilercc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
AOT compile1188.9 s
binary6.0 MiB
C warnings5
spinel warnings23

Host

hostshowcase.party
cpuAMD Ryzen 5 3600 6-Core Processor
cores12
kernelLinux 6.8.0-136-generic

Sequence

visits/iter114
warmup15
roundhouse iters76
rails iters21

Generated 2026-07-30T22:35:23Z from summary.json.