Ruby 4.0 YJIT vs ZJIT: Performance Guide
Ruby 4.0 ships two JIT compilers. What YJIT delivers on Rails apps in 2026, the two flags worth tuning, and when the experimental ZJIT is worth a try.

Updated August 2026. This guide originally covered YJIT on Ruby 3.4. Ruby 4.0 shipped on December 25, 2025 with a second JIT compiler, so the advice below now covers both.
Ruby 4.0 ships two JIT compilers in the same binary. YJIT is the one your Rails app is probably already running without you having configured anything. ZJIT is its successor, and the release notes that introduce it also tell you not to deploy it.
If you’re mid-upgrade, that combination reads as a contradiction. Sorting it out takes about ten minutes of checking and, on most apps, zero new flags.
You may already be running YJIT #
Every Rails 8 app has YJIT on out of the box whenever it boots on Ruby 3.3 or newer - the default arrived back in Rails 7.2 as a one-line initializer inside the framework defaults, and it lands silently for anyone who upgrades and runs load_defaults at 7.2 or later.
So before tuning anything, check:
bin/rails runner 'puts RubyVM::YJIT.enabled?'
If that prints false on Ruby 3.3+, the usual culprit is an app that climbed to Rails 8 through upgrades without ever adopting the newer framework defaults. Set config.yjit = true in config/application.rb, or enable it late in boot:
# config/initializers/enable_yjit.rb
RubyVM::YJIT.enable
Enabling after boot skips a cost the --yjit flag pays: initializer and boot-time code runs once, and compiling it wastes JIT memory. On Ruby 4.0, RubyVM::YJIT.enable also accepts mem_size: and call_threshold: keywords, added in the 4.0 release, so you can tune it without touching command-line flags.
Plenty of upgrade checklists end right here: it was already on.
What YJIT is worth in 2026 #
On the official benchmark suite at speed.ruby-lang.org (numbers as of August 2026 - it’s a live dashboard, so expect drift), current YJIT runs the headline benchmarks at roughly 2x interpreter speed, and railsbench at about 2.2x. The same dashboard shows YJIT on the 4.x line beating YJIT 3.4.7 by 8.4% geomean, with railsbench 17.1% faster, so the 3.4 to 4.0 upgrade is itself a performance change even if you touch nothing else.
The benchmark says 2x; your app may measure a single-digit gain. Both numbers are honest. Railsbench is CPU-bound Ruby; your checkout endpoint spends most of its time waiting on Postgres and Stripe, and no JIT speeds up waiting. The Rails team’s own framing when they made YJIT the default was 15-25% latency improvement on real applications. The win lands on Ruby-heavy paths like serialization and view rendering, not on I/O.
The same logic applies in reverse: a database-bound endpoint needs database work, not a JIT flag. Cheaper wins usually live in the infrastructure around Ruby - moving jobs to Solid Queue, dropping Redis for Solid Cache, or switching to Falcon when the bottleneck is waiting on I/O.
YJIT costs memory. Compiled machine code and its metadata are capped by --yjit-mem-size, 128 MiB by default, on top of your app’s normal footprint. On a 512 MB container that is not a rounding error, and it is the first number to revisit when a post-upgrade pod starts flirting with its memory limit.
The two flags that matter #
YJIT exposes a dozen options. Two of them earn attention on a production Rails app, per the official YJIT documentation:
--yjit-mem-size(default 128 MiB) - the soft cap on everything YJIT allocates. Raise it if stats show compilation stopping early; lower it on small containers.--yjit-call-threshold(default 30, rising automatically to 120 on apps with over 40k ISEQs) - how many calls before a method compiles. Most apps never need to touch it.
To see whether any of this is working, run with --yjit-stats=quiet and read the counters:
stats = RubyVM::YJIT.runtime_stats
stats[:ratio_in_yjit] # % of instructions run as machine code; healthy apps sit near 99
stats[:code_region_size] # bytes of generated code, your mem-size budget in action
stats[:side_exit_count] # how often compiled code fell back to the interpreter
A ratio_in_yjit in the low 90s usually means the memory cap bit before compilation finished. Raise mem_size: and measure again.
Compiled code dies with the process. A worker-killer that recycles Pumas every 30 minutes throws away every compiled block and re-pays the warmup, so the YJIT docs recommend letting processes live as long as memory allows. If you added aggressive worker recycling years ago to mask a leak, it is now taxing your JIT too.
ZJIT: the successor you should test, not deploy #
Ruby 4.0’s second compiler is ZJIT, built by the same Shopify team as the next generation of YJIT. Where YJIT compiles small basic blocks lazily, ZJIT compiles whole methods through an SSA intermediate representation - a deliberately textbook design that, per the launch post, exists so more compiler engineers can contribute to it. The same post details what the bigger compilation unit buys: inline versions of well-known C methods, and register spilling that lets it handle enormous functions. When its type assumptions break, it side-exits back to the interpreter.
What it cannot do yet is beat YJIT. The release notes are blunt: ZJIT is faster than the interpreter but not as fast as YJIT, and the stated goal for Ruby 4.1 is to make ZJIT faster than YJIT and production-ready. The launch post goes further: “You should expect crashes and wild performance degradations (or, perhaps, improvements).”
Trying it takes one flag on a canary or staging box:
ruby --zjit myscript.rb
# or
RUBY_ZJIT_ENABLE=1 bin/rails server
# or at runtime
RubyVM::ZJIT.enable
Building Ruby from source with ZJIT needs Rust 1.85.0 or newer; a prebuilt binary only includes it if it was compiled that way, which official releases are.
ZJIT’s team improves it against reports from real workloads, and a staging box that mirrors your traffic produces exactly those reports.
When not to enable a JIT #
Short-lived processes rarely repay compilation. A rake task that runs for 40 seconds spends its life below the call threshold or throwing away code it just compiled.
Watch the trap here: a config/initializers/ enable runs for rake tasks and rails runner too, not just the server - initializers execute whenever the Rails environment loads. If your cron boxes are memory-tight, guard the call or move it somewhere only the server executes, like Puma’s config file. Otherwise leave the framework default on and size memory for it.
Test suites are a judgment call. Spec processes are short-lived and restart constantly, so the warmup often costs more than the speedup returns; if CI time matters, benchmark one run with YJIT disabled before assuming the default helps you there.
And on memory-starved containers, do the arithmetic first. The JIT’s headroom has to come from somewhere, and an OOM-killed worker is slower than interpreted Ruby. CPU spent inside Ruby is also worth auditing before you tune the compiler that runs it - compression settings on encrypted columns are a classic example of cores burning where no JIT flag fixes anything.
The short version #
Check RubyVM::YJIT.enabled? before changing anything, because Rails has probably already decided for you. Upgrade to Ruby 4.0 and take the free 17% on Rails-shaped workloads. Leave ZJIT out of production this year, but give it a staging box and file what you find - Ruby 4.1 is where the successor is supposed to overtake its parent.
If you want a second pair of eyes on a Rails app whose response times stopped making sense, our Rails development team does this work: profiling first, JIT flags only when the profile says Ruby CPU is the bottleneck.
Sources #
- Shopify Engineering, Ruby 3.2’s YJIT is Production-Ready (January 2023). 38% speedup on railsbench over the interpreter; YJIT 57% faster than Ruby 3.1.3.
- Rails at Scale: Ruby 3.3’s YJIT Runs Shopify’s Production Code 15% Faster (September 2023). Production measurement: 15.8–19.6% speedup on Storefront Renderer at Shopify scale.
- Shopify, yjit-metrics. Continuously updated YJIT speedups — the raw data behind every benchmark in this post.
- speed.ruby-lang.org. Live dashboard: YJIT vs. interpreter across microbenchmarks, Railsbench, and yjit-bench.
- ZJIT launch post on Rails at Scale. Architecture and roadmap from the team building Ruby 4.0’s successor JIT.
- Ruby 4.0.0 release notes — primary source for what changed.
- Official YJIT documentation — every flag, counter, and
--yjit-statsfield referenced above. - Rails 7.2 release announcement — the release that made YJIT the default JIT for new Rails apps.
Reading this because something is going wrong?
A free code audit gives you a written assessment of your codebase in plain English.
Get a Free Code AuditRated 4.8/5 on Clutch · you keep the write-up either way