Skip to content

Performance

Performance

go-ruby-rack/rack is the pure-Go library that rbgo binds for Ruby’s rack. This page records the module’s place in the ecosystem-wide per-module parity suite: the same Ruby-visible operation is run through the pure-Go library and through each reference runtime’s own rack gem; the Go output is verified byte-identical to MRI first, then timed.

The harness lives under benchmarks/ in this repository — a self-contained Go driver that pins the published library by pseudo-version (no replace), the equivalent Ruby workload over the real rack gem, and a run.sh that gates on byte-identical parity before timing.

Setup

Date2026-07-05
HostApple M4 Max, 16 cores, macOS 26.5.1 (arm64), on the host — no VM
Go1.26.4 (CGO_ENABLED=0), library github.com/go-ruby-rack/rack v0.0.0-20260705195519-3d6eb0d8d96b
MRIruby 4.0.5 +PRISM, with and without --yjit
JRuby10.1.0 (OpenJDK 25.0.2)
TruffleRuby34.0.1 (GraalVM CE Native, like ruby 3.4.9)
rack gem3.2.6 on every reference runtime

Method. Each process runs 3 untimed warm-up passes (to let the JVM/GraalVM JITs warm up), then 25 timed passes of a fixed inner loop, timed with a monotonic clock; the best pass is reported as ns/op (best, not mean, to suppress scheduler noise). Interpreter start-up is outside the timed region. Every op’s output is checked byte-identical to MRI before any timing is recorded — the structural ops (parse_*, request, response) through a length-prefixed canonical serialiser shared by the Go and Ruby drivers.

Reading the numbers

JRuby and TruffleRuby are timed cold / single-shot (one short-lived process, best of 25 passes) and can understate their true steady-state peak; their columns are indicative, not their JIT ceiling. Sub-microsecond rows carry the most relative noise and should be read as order-of-magnitude. Every number below is a real measured value from the dated run above — nothing estimated or cherry-picked.

Headline: pure-Go vs MRI + YJIT

Rack’s compute surface is pure Ruby, so the pure-Go port is broadly far ahead of even the JIT. go-ruby-rack beats MRI + YJIT on all 9 ops. The closest race is escape_html, where Rack::Utils.escape_html dispatches into a C-implemented core (cgi/escape): a two-pass strings.Replacer used to lose to it, but the table-driven single-pass escaper (a 256-entry byte→entity lookup that bulk-copies verbatim runs and returns no-escape input allocation-free) now runs ~1.5× ahead of even YJIT’s C path.

Opgo-ruby (ns/op)MRI + YJIT (ns/op)go vs YJIT
escape245.74999.220.3× faster
unescape116.25659.648.7× faster
escape_html104.8162.21.55× faster
parse_query1798.68985.55.00× faster
build_query779.95386.56.91× faster
parse_nested_query1735.47001.54.03× faster
build_nested_query1029.47337.57.13× faster
request2310.68559.53.70× faster
response854.82585.53.02× faster

Full results

Nine representative primitives from Rack’s pure-compute surface, over fixed inputs present in both go-ruby-rack and rack 3.2.6. (unescape_html is omitted — it is not part of Rack::Utils 3.2.6.) vs MRI is the ratio to plain MRI (lower is faster).

escape

Rack::Utils.escape — form-encode a realistic value (spaces, reserved bytes, multi-byte UTF-8).

Runtimens/opvs MRI
go-ruby (pure Go)245.70.05×
MRI5213.21.00×
MRI + YJIT4999.20.96×
JRuby2231.50.43×
TruffleRuby3844.00.74×

unescape

Rack::Utils.unescape — the inverse of escape.

Runtimens/opvs MRI
go-ruby (pure Go)116.20.02×
MRI5583.41.00×
MRI + YJIT5659.61.01×
JRuby3267.40.59×
TruffleRuby3983.90.71×

escape_html

Rack::Utils.escape_html — the five-character HTML-entity escape (&, <, >, ", '). MRI/YJIT dispatch to a C core (cgi/escape) here, so this is the closest race — and the pure-Go table-driven escaper now wins it outright.

Runtimens/opvs MRI
go-ruby (pure Go)104.80.55×
MRI191.81.00×
MRI + YJIT162.20.85×
JRuby304.41.59×
TruffleRuby2065.310.77×

parse_query

Rack::Utils.parse_query — a flat query string (repeated array keys, %/+ values, a valueless key) into a key → value(s) map.

Runtimens/opvs MRI
go-ruby (pure Go)1798.60.17×
MRI10550.51.00×
MRI + YJIT8985.50.85×
JRuby4354.30.41×
TruffleRuby13347.31.27×

build_query

Rack::Utils.build_query — the inverse of parse_query.

Runtimens/opvs MRI
go-ruby (pure Go)779.90.12×
MRI6628.01.00×
MRI + YJIT5386.50.81×
JRuby2440.70.37×
TruffleRuby6311.90.95×

parse_nested_query

Rack::Utils.parse_nested_query — a foo[bar][]=… nested body into structural Hash/Array/String/nil.

Runtimens/opvs MRI
go-ruby (pure Go)1735.40.19×
MRI9019.51.00×
MRI + YJIT7001.50.78×
JRuby3418.70.38×
TruffleRuby5787.00.64×

build_nested_query

Rack::Utils.build_nested_query — the inverse of parse_nested_query.

Runtimens/opvs MRI
go-ruby (pure Go)1029.40.11×
MRI9012.01.00×
MRI + YJIT7337.50.81×
JRuby3417.90.38×
TruffleRuby7747.80.86×

request

Build a Rack::Request from a fixed env Hash and read a fixed set of accessors (request_method, host, port, scheme, ssl?, media_type, xhr?, url, …).

Runtimens/opvs MRI
go-ruby (pure Go)2310.60.17×
MRI13675.01.00×
MRI + YJIT8559.50.63×
JRuby6967.30.51×
TruffleRuby10863.80.79×

response

Build a Rack::Response, write a body, and finish it into the SPEC [status, headers, body] tuple (content-length filled in).

Runtimens/opvs MRI
go-ruby (pure Go)854.80.21×
MRI4083.51.00×
MRI + YJIT2585.50.63×
JRuby2024.50.50×
TruffleRuby4284.71.05×

Reproduce

bash benchmarks/run.sh

The script re-verifies byte-identical parity with MRI, then re-runs every available runtime. Knobs: OUTER (timed passes, default 25), WARM (warm-up passes, default 3), and RUBY/JRUBY/TRUFFLERUBY to select binaries.