Ruby 4.0: ZJIT, Ruby Box, and Ractors

Ruby 4.0: ZJIT, Ruby Box, and Ractors

Ruby 4.0 was released on December 25, 2025, and three features get cited most often when people talk about it: ZJIT, Ruby Box, and a reworked Ractor API.

Two of those three features are experiments you have to turn on deliberately. The third one removes methods.

In this article, we will go through what ZJIT, Ruby Box, and the new Ractor::Port API actually do, and which of them can force you to change code when you move from Ruby 3.4 to 4.0.

ZJIT

ZJIT is a new just-in-time compiler, described in the Ruby 4.0.0 release announcement opens a new window as the next generation of YJIT and built by the same team. You enable it with the --zjit command line flag, at runtime with RubyVM::ZJIT.enable or even adding the environment variable RUBY_ZJIT_ENABLE. Building Ruby with ZJIT support requires Rust 1.85.0 or newer, according to the Ruby 4.0.0 release notes opens a new window , so any Ruby image built without a recent enough Rust toolchain will not have it available.

Where it stands today comes from the announcement itself: “ZJIT is faster than the interpreter, but not yet as fast as YJIT.” The Ruby team encourages people to experiment with it while suggesting you “maybe hold off on deploying it in production for now.” The stated goal is to make ZJIT faster than YJIT and production-ready in future Ruby versions.

How ZJIT differs from YJIT

Both compile Ruby to machine code while your program runs. The difference is how much code to look at when they do it.

Neither one starts from your source. Ruby compiles that to bytecode for YARV opens a new window (Yet another Ruby VM), the virtual machine at the core of standard Ruby (CRuby), and both JITs work from the bytecode rather than from the text you wrote.

YJIT compiles one basic block at a time, a basic block being a straight run of bytecode with no branches in or out of the middle of it. It uses lazy basic block versioning (LBBV) to profile types: it follows the paths your code actually takes, observes the types flowing through each block, and emits a version specialized for what it saw. Those blocks go straight to a low-level intermediate representation (LIR), one step above the machine instructions YJIT ends up emitting. Type specialization is cheap and local that way, but the optimizer only ever sees a fragment of a method at a time.

ZJIT compiles an entire method at a time, and it puts a second representation in the middle: a high-level IR (HIR) sitting between YARV bytecode and LIR. HIR is in static single assignment form (SSA), where every variable is assigned exactly once, which is the shape most textbook compiler optimizations are written against. ZJIT also sources its types differently, reading what the interpreter recorded while running your code instead of deriving them through LBBV. The payoff is a modular optimizer that runs over HIR while lowering it to LIR, with a whole method in view rather than a block.

The release announcement puts it in one sentence: “We want to both raise the performance ceiling (bigger compilation unit size and SSA IR) and encourage more outside contribution (by becoming a more traditional method compiler).”

Ruby Box

The Ruby Box documentation opens a new window describes it as a way “to provide separated spaces in a Ruby process, to isolate application code, libraries and monkey patches.” It is listed as experimental in the Ruby 4.0.0 release notes opens a new window , and it only exists if you start Ruby with RUBY_BOX=1, which also prints an experimental warning.

The idea is easier to see in code than in the abstract. You create a box, require code into it, and then reach that code through the box itself:

box = Ruby::Box.new
box.require('something')

p box::Something

Whatever that file defines stays inside the box: its classes and modules, its constants and global variables, its top-level methods, and any monkey patches it applies to built-in classes. String and Array are still available inside a box, but if the code in the box reopens String and adds a method, that method does not exist outside the box. A gem could patch a core class without the patch leaking into the rest of your process.

If your application has a specific monkey patching problem you have been unable to contain, the documentation is worth reading and experimenting with.

Ractor::Port

Ractors are where Ruby 4.0 actually breaks things. Ractors are Ruby’s way of running Ruby code in parallel, added as an experimental feature in Ruby 3.0, they achieve this by keeping state separate: each one owns its own objects, and instead of sharing memory they pass messages to each other. That makes them a reasonable fit for CPU-bound work you can split into independent pieces, like resizing a batch of images or parsing a large set of files.

What Ruby 4.0 changes is how Ractors talk to each other. Communication now goes through a new Ractor::Port class, added in Feature #21262 opens a new window . A port is an object you create, hand to a Ractor, and read from later, and the Ruby 4.0.0 release notes opens a new window list its four methods: receive, send (also written <<), close, and closed?.

Adding ports is also what took four methods away:

  • Ractor.yield
  • Ractor#take
  • Ractor#close_incoming
  • Ractor#close_outgoing

Under the old API, a Ractor pushed a value out with Ractor.yield and the other side pulled it with take:

# Ruby 3.x, no longer valid in 4.0
ractor = Ractor.new do
  Ractor.yield(compute_something)
end

result = ractor.take

With the new API, the value travels through an explicit port instead:

# Ruby 4.0
port = Ractor::Port.new

ractor = Ractor.new(port) do |port|
  port << compute_something
end

result = port.receive

If what you need is a single value back, Ruby 4.0 also adds Ractor#value, which the release notes describe as similar to Thread#value.

Ractors have been experimental since 3.0 and the API has changed more than once in that time, so anything written against the 3.x API needs a pass before it runs on 4.0.

Upgrading to Ruby 4.0

The only feature that might actually break your code is the Ractor’s API. If your gem or application depends on that API directly, you’ll need to make sure you’re using the new 4.0 API.

Beyond the headline features, the release has the usual crop of compatibility items to run through: ObjectSpace._id2ref is deprecated, Process::Status#& and Process::Status#>> are removed after being deprecated since 3.3, the CGI library is gone except for its escape functions, and Windows builds now require Visual Studio 2015 or later. Running your test suite with deprecation warnings enabled on Ruby 3.4 first will surface most of what affects you, which is the same advice that applies to every Ruby upgrade and is covered in more depth in our step-by-step guide to upgrading Ruby opens a new window .

Conclusion

In this article, we went through the three main features that define this release. ZJIT is a method-based JIT with an SSA IR that trades YJIT’s fine-grained specialization for a bigger compilation unit and a more conventional optimizer. Ruby Box is an isolation mechanism that proposes to solve the problem of managing multiple overrides of the same classes, in particular Ruby’s core classes. Finally, Ractors had a major API change and introduced a new mechanism for their communication with Ractor::Port. None of this blocks an upgrade unless your application uses the old Ractor API.

Is your application still a few Ruby versions behind? We can help you get up to date! opens a new window

Get the book