35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Ruby YJIT in Production: How to Enable It and What Performance Gains to Expect

Ruby YJIT in Production: How to Enable It and What Performance Gains to Expect

TTB Software
ruby, performance
A practical guide to enabling YJIT in Ruby 3.3 for production Rails apps. Covers configuration, memory trade-offs, real benchmarks, and monitoring tips.

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, and define_method
  • The --yjit-exec-mem-size flag 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:

  1. Enable YJIT — one flag, 15-25% faster (you’re here)
  2. Tune Ruby GC — environment variables, 5-15% faster
  3. Fix N+1 queries — code changes, potentially 10x faster on affected pages
  4. Add database indexes — migrations, dramatic improvement on slow queries
  5. 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.

#ruby #yjit #performance #jit #production
T

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 Touch

Share this article

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