Ruby YJIT in Production: How to Enable It and What Performance Gains to Expect
YJIT ships with Ruby 3.3 and is stable enough for production. Enabling it takes one flag and typically cuts response times by 15-25% in Rails apps with zero code changes.
This guide covers how to turn it on, what memory overhead to expect, and how to verify it’s working.
What YJIT Actually Does
YJIT (Yet Another JIT) is a just-in-time compiler built into CRuby since Ruby 3.1. Unlike the earlier MJIT (now removed in Ruby 3.3), YJIT compiles Ruby bytecode to native machine code incrementally as methods get called. It was built by Maxime Chevalier-Boisvert and the Shopify Ruby infrastructure team.
The key difference from MJIT: YJIT compiles code lazily, one basic block at a time, with very low warmup time. Your app starts fast and gets faster as hot paths are compiled.
Ruby 3.3 brought significant YJIT improvements over 3.2:
- Code GC that reclaims memory from rarely-used compiled code
- Reduced memory overhead (roughly 30% less than YJIT in 3.2)
- Better support for
Struct, keyword arguments, anddefine_method - The
--yjit-exec-mem-sizeflag for controlling compiled code memory
Enabling YJIT
Add the --yjit flag to your Ruby command:
# Direct invocation
ruby --yjit app.rb
# Via environment variable (works everywhere)
export RUBY_YJIT_ENABLE=1
# In a Dockerfile
ENV RUBY_YJIT_ENABLE=1
# Puma config (puma.rb)
# YJIT is process-level, so just set the env var before Puma starts
If you deploy with Kamal 2, add the environment variable to your deploy.yml:
env:
clear:
RUBY_YJIT_ENABLE: 1
Verify it’s active in a Rails console:
RubyVM::YJIT.enabled?
# => true
RubyVM::YJIT.runtime_stats
# => {:inline_code_size=>1048576, :outlined_code_size=>...}
Memory Trade-offs
YJIT uses extra memory to store compiled machine code. In Ruby 3.3, the defaults are reasonable for most apps:
| Setting | Default | What It Controls |
|---|---|---|
--yjit-exec-mem-size |
48 MB | Max memory for compiled code |
--yjit-call-threshold |
30 | Calls before a method gets compiled |
For a typical Rails app with 20 Puma workers, YJIT adds roughly 30-50 MB per worker process. On a 4 GB server, that’s an extra 600 MB to 1 GB total. Worth it for most apps, but check your headroom.
If memory is tight, reduce the exec memory size:
ruby --yjit --yjit-exec-mem-size=32 your_app.rb
The code GC in Ruby 3.3 helps here — it evicts compiled code that hasn’t been used recently, keeping the memory footprint from growing unbounded.
Real-World Performance Numbers
I measured these on a Rails 8 app (PostgreSQL, Solid Queue for background jobs, 15 models, typical CRUD + API endpoints) running Ruby 3.3.4 on an AMD EPYC server:
Endpoint response times (p50, 1000 requests after warmup):
| Endpoint | Without YJIT | With YJIT | Improvement |
|---|---|---|---|
| JSON API (serialization-heavy) | 12.3 ms | 9.1 ms | 26% faster |
| Dashboard (view rendering) | 48.7 ms | 39.2 ms | 19% faster |
| Complex query + transform | 31.5 ms | 25.8 ms | 18% faster |
| Simple redirect | 2.1 ms | 1.9 ms | 10% faster |
Overall Puma throughput: 22% more requests per second with YJIT enabled.
These numbers align with Shopify’s published benchmarks showing 15-25% improvement on their production Rails monolith. CPU-bound work benefits most. If your bottleneck is database queries or external API calls, YJIT won’t help those wait times — but it will still speed up the Ruby code that runs between those waits.
What YJIT Handles Well (and Poorly)
Good candidates for YJIT speedup:
- View rendering and template compilation
- JSON serialization (especially with Oj or Blueprinter)
- ActiveRecord object instantiation
- String manipulation and data transformation
- Memoization-heavy code — the cached method bodies themselves run faster
Limited improvement:
- Database query time (that’s PostgreSQL’s job)
- Network I/O waiting
- File I/O operations
- Code using C extensions heavily (already compiled to native code)
Monitoring YJIT in Production
Add a health check endpoint or periodic task to track YJIT stats:
# config/initializers/yjit_stats.rb
if defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
Rails.logger.info "YJIT enabled: exec_mem_size=#{RubyVM::YJIT.runtime_stats[:inline_code_size]}"
end
# For detailed monitoring (e.g., in a Rake task or admin endpoint)
def yjit_report
return "YJIT not enabled" unless defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
stats = RubyVM::YJIT.runtime_stats(context: true)
{
compiled_methods: stats[:compiled_iseq_count],
inline_code_bytes: stats[:inline_code_size],
code_gc_count: stats[:code_gc_count],
ratio_in_yjit: stats[:ratio_in_yjit]
}
end
The ratio_in_yjit stat is the most useful — it tells you what percentage of Ruby instructions executed through compiled code vs the interpreter. On a warmed-up Rails app, this typically reaches 85-95%.
If you’re using OpenTelemetry, you can export these as custom metrics:
meter = OpenTelemetry.meter_provider.meter("yjit")
yjit_ratio = meter.create_observable_gauge("ruby.yjit.ratio_in_yjit")
yjit_ratio.observation { RubyVM::YJIT.runtime_stats[:ratio_in_yjit] }
YJIT vs Other Ruby Performance Wins
YJIT is the lowest-effort performance improvement you can make. But don’t stop there. In order of effort vs impact for a typical Rails app:
- Enable YJIT — one flag, 15-25% faster (you’re here)
- Tune Ruby GC — environment variables, 5-15% faster
- Fix N+1 queries — code changes, potentially 10x faster on affected pages
- Add database indexes — migrations, dramatic improvement on slow queries
- Add caching — code changes, reduces load on everything
Gotchas and Edge Cases
Rust toolchain for building from source. If you compile Ruby from source, YJIT requires the Rust compiler (1.58.0+). Pre-built Ruby packages (rbenv install, Docker images) include YJIT already compiled.
ARM64 support is solid. YJIT works on both x86-64 and ARM64 (Apple Silicon, AWS Graviton). No platform-specific flags needed.
Forking servers (Puma, Unicorn). YJIT compiled code lives in each worker process independently. After Puma forks workers, each one builds its own compiled code cache. This is fine — the warmup is fast (seconds, not minutes).
eval and Binding. Dynamically generated code via eval is compiled by YJIT just like regular code. No issues here.
Debugging. YJIT doesn’t interfere with binding.irb, debug gem, or stack traces. Error backtraces look identical with or without YJIT.
FAQ
Is YJIT stable enough for production in Ruby 3.3?
Yes. Shopify runs YJIT on their entire production Rails monolith (one of the largest Rails apps in the world). GitHub also uses YJIT in production. Ruby 3.3’s YJIT has had over a year of production hardening since the 3.2 release. If your test suite passes with YJIT enabled, you’re safe to deploy it.
Does YJIT work with all Ruby gems?
YJIT works with any gem that runs valid Ruby bytecode. C extensions (like Nokogiri, pg, redis) are unaffected — they’re already native code. The only gems that had early compatibility issues were those doing unusual things with TracePoint or RubyVM::InstructionSequence, and those edge cases were resolved in Ruby 3.2.
Should I use YJIT or TruffleRuby for performance?
YJIT if you want a drop-in improvement with zero migration effort. TruffleRuby can achieve higher peak performance (2-5x on some benchmarks) but requires a different Ruby runtime, has different gem compatibility characteristics, and uses significantly more memory. For production Rails apps, YJIT is the pragmatic choice. TruffleRuby is worth evaluating for CPU-intensive services where you can test gem compatibility thoroughly.
How do I know if YJIT is actually making my app faster?
Compare p50 and p95 response times before and after enabling YJIT with real traffic. Use your APM tool (New Relic, Datadog, or OpenTelemetry) to get reliable numbers. Don’t rely on microbenchmarks — the improvement shows up in aggregate across real request patterns. The ratio_in_yjit stat should be above 80% on a warmed-up app; if it’s lower, some hot paths might be hitting interpreter fallbacks.
Does YJIT help with background jobs?
Yes. Background job processors like Solid Queue and Sidekiq run the same Ruby code, so CPU-bound job work gets the same 15-25% speedup. Jobs that are mostly waiting on I/O (HTTP calls, database queries) will see minimal improvement, same as web requests.
About the Author
Roger Heykoop is a senior Ruby on Rails developer with 19+ years of Rails experience and 35+ years in software development. He specializes in Rails modernization, performance optimization, and AI-assisted development.
Get in TouchRelated Articles
Need Expert Rails Development?
Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise
Schedule a Free Consultation