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.
Each endpoint is its own chart. Bars are log-scaled; raw req/sec is shown at the right.
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.
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.
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.
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.
Interpreter-bound targets (ruby, rails) get only 1.5–1.8× because per-call dispatch overhead dominates whether the work is helpers or serialization. Compiled targets show the helper-chain cost more clearly because their serialization is fast — Go's gap is the largest of the compiled set even after the IR string-builder annotation pass closed part of it.
Max RSS observed across all endpoints, per target. Rust and crystal sit under 30 MB; ruby and go are an order of magnitude higher; rails and typescript carry roughly another order on top of that. The cost-economics chart above (section 4) compounds these into req/sec/GB, which spans three orders of magnitude end-to-end.
Latencies are c=64 concurrent connections; treat p50 as median per-connection wait, p99 as tail. Only rust hits sub-millisecond on any endpoint; crystal lands 1–4 ms, go and typescript 3–17 ms, ruby 9–22 ms, rails 45–135 ms. The order matches the throughput chart inversely, as expected.
| target | /articles | /articles/1 | /articles/new | /articles.json | /articles/1.json | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| p50 | p99 | p50 | p99 | p50 | p99 | p50 | p99 | p50 | p99 | |
| rust | 1.7 | 3.1 | 0.9 | 2.2 | 0.6 | 1.5 | 1.0 | 1.9 | 0.4 | 1.0 |
| crystal | 3.4 | 4.1 | 2.3 | 3.2 | 1.2 | 1.6 | 2.4 | 3.3 | 1.2 | 1.7 |
| go | 8.3 | 21.6 | 8.0 | 18.5 | 3.2 | 45.6 | 3.6 | 9.9 | 1.9 | 8.1 |
| spinel | 7.3 | 19.9 | 5.6 | 21.5 | 3.5 | 20.8 | 4.3 | 22.1 | 2.8 | 22.5 |
| typescript | 7.4 | 8.7 | 6.4 | 7.7 | 3.3 | 3.6 | 6.0 | 7.3 | 3.9 | 4.8 |
| ruby | 13.1 | 15.6 | 12.2 | 14.2 | 9.0 | 11.4 | 11.8 | 14.9 | 8.9 | 11.5 |
| ruby-int | 17.1 | 20.6 | 15.5 | 18.0 | 11.4 | 13.7 | 15.1 | 18.0 | 11.4 | 13.5 |
| rails | 127.1 | 238.0 | 130.6 | 373.9 | 88.9 | 160.4 | 72.8 | 93.2 | 47.0 | 69.8 |
| rails-int | 125.2 | 222.4 | 130.6 | 265.8 | 88.6 | 163.1 | 69.8 | 87.5 | 45.4 | 135.0 |
All values in milliseconds. Lower is better.
| target | endpoint | req/sec | p50 (ms) | p99 (ms) | RSS (MB) | req/sec/GB |
|---|---|---|---|---|---|---|
| rust | /articles | 38,007 | 1.66 | 3.07 | 17 | 2,226,960 |
| rust | /articles/1 | 67,690 | 0.87 | 2.19 | 18 | 3,820,963 |
| rust | /articles/new | 97,083 | 0.58 | 1.51 | 18 | 5,420,618 |
| rust | /articles.json | 62,048 | 1.01 | 1.88 | 18 | 3,426,492 |
| rust | /articles/1.json | 142,821 | 0.37 | 1.01 | 18 | 7,804,829 |
| crystal | /articles | 19,217 | 3.42 | 4.10 | 26 | 735,079 |
| crystal | /articles/1 | 25,828 | 2.33 | 3.15 | 25 | 1,042,069 |
| crystal | /articles/new | 48,983 | 1.20 | 1.62 | 21 | 2,374,372 |
| crystal | /articles.json | 25,236 | 2.42 | 3.26 | 21 | 1,205,375 |
| crystal | /articles/1.json | 49,037 | 1.25 | 1.67 | 21 | 2,339,387 |
| go | /articles | 7,343 | 8.27 | 21.64 | 32 | 229,661 |
| go | /articles/1 | 7,641 | 7.98 | 18.48 | 33 | 231,869 |
| go | /articles/new | 10,088 | 3.18 | 45.65 | 33 | 304,391 |
| go | /articles.json | 16,543 | 3.59 | 9.87 | 35 | 471,170 |
| go | /articles/1.json | 29,349 | 1.92 | 8.09 | 35 | 846,039 |
| spinel | /articles | 7,464 | 7.33 | 19.87 | 33 | 225,597 |
| spinel | /articles/1 | 8,579 | 5.62 | 21.52 | 36 | 237,809 |
| spinel | /articles/new | 12,093 | 3.52 | 20.79 | 38 | 319,029 |
| spinel | /articles.json | 10,199 | 4.26 | 22.12 | 39 | 266,405 |
| spinel | /articles/1.json | 15,231 | 2.85 | 22.51 | 42 | 366,123 |
| typescript | /articles | 8,438 | 7.44 | 8.72 | 367 | 23,510 |
| typescript | /articles/1 | 9,786 | 6.40 | 7.68 | 364 | 27,514 |
| typescript | /articles/new | 19,189 | 3.31 | 3.64 | 364 | 53,936 |
| typescript | /articles.json | 10,396 | 6.02 | 7.30 | 365 | 29,108 |
| typescript | /articles/1.json | 15,604 | 3.88 | 4.84 | 365 | 43,686 |
| ruby | /articles | 4,858 | 13.09 | 15.59 | 116 | 42,526 |
| ruby | /articles/1 | 5,223 | 12.21 | 14.15 | 129 | 41,313 |
| ruby | /articles/new | 7,018 | 9.04 | 11.45 | 138 | 52,055 |
| ruby | /articles.json | 5,341 | 11.82 | 14.87 | 137 | 39,688 |
| ruby | /articles/1.json | 7,116 | 8.86 | 11.47 | 150 | 48,536 |
| ruby-int | /articles | 3,725 | 17.10 | 20.64 | 105 | 36,130 |
| ruby-int | /articles/1 | 4,114 | 15.51 | 18.03 | 116 | 36,188 |
| ruby-int | /articles/new | 5,562 | 11.45 | 13.72 | 126 | 44,897 |
| ruby-int | /articles.json | 4,204 | 15.12 | 17.99 | 127 | 33,713 |
| ruby-int | /articles/1.json | 5,595 | 11.41 | 13.53 | 140 | 40,827 |
| rails | /articles | 484 | 127.07 | 238.03 | 320 | 1,546 |
| rails | /articles/1 | 473 | 130.62 | 373.88 | 334 | 1,448 |
| rails | /articles/new | 699 | 88.87 | 160.44 | 339 | 2,111 |
| rails | /articles.json | 877 | 72.80 | 93.20 | 345 | 2,598 |
| rails | /articles/1.json | 1,290 | 46.95 | 69.81 | 393 | 3,354 |
| rails-int | /articles | 494 | 125.22 | 222.38 | 302 | 1,673 |
| rails-int | /articles/1 | 475 | 130.60 | 265.84 | 314 | 1,548 |
| rails-int | /articles/new | 700 | 88.63 | 163.12 | 328 | 2,186 |
| rails-int | /articles.json | 900 | 69.84 | 87.46 | 334 | 2,758 |
| rails-int | /articles/1.json | 1,329 | 45.44 | 134.97 | 383 | 3,545 |