
May 14, 2026
In-depth technical analysis · RubyStackNews · Concurrency & Performance
For decades, the Global VM Lock (GVL) — also known as the GIL — was CRuby’s great concession: the safety and simplicity of an object model free of data races, in exchange for not being able to execute Ruby code in parallel within the same process. Ractors, introduced in Ruby 3.0, change that equation for the first time.
What Is a Ractor?
A Ractor is a parallel execution unit with object isolation. It looks superficially like a Thread, but its object-sharing rules are radically more restrictive — and that is exactly what allows CRuby to hold a GVL per ractor instead of one global lock.
Creating one is straightforward. A Ractor takes a block and can run in parallel with others:
r = Ractor.new { puts "Hello from Ractor #{Ractor.current}" }r.join # wait for it to finish
The fundamental difference from threads: a Ractor cannot access variables from the outer scope. The following code fails immediately:
a = 1Ractor.new { puts a }# Ractor::IsolationError: can not isolate a Proc because it# accesses outer variables (a).
Why this matters
Passing Objects Between Ractors
Communication happens exclusively through messages. There are two mechanisms: passing arguments to Ractor.new, or the message-passing model with send / receive.
Constructor arguments
a = 42r = Ractor.new(a) do |value| puts "Received: #{value}"endr.join
send / receive (message passing)
r = Ractor.new do msg = Ractor.receive # blocks until a message arrives puts "Processing: #{msg}"endr.send("work")r.join
send is non-blocking: it returns immediately even if the receiving ractor is not yet waiting. receive, on the other hand, blocks the current ractor’s thread until a message arrives.
Ractors solve parallelism at the design level: if an object cannot be shared, the runtime tells you before the bug ever reaches production.
Shareable vs. Unshareable Objects
The heart of the Ractor model is the distinction between shareable and unshareable objects. You can check with Ractor.shareable?:
Ractor.shareable?(1) #=> true — numbers and other basic immutablesRactor.shareable?('hello') #=> false — mutable stringRactor.shareable?('hello'.freeze) #=> trueRactor.shareable?([Object.new].freeze) #=> false — inner object is mutable
To make a complex object shareable (including its internal parts), use Ractor.make_shareable, which recursively freezes the object:
ary = ['hello', 'world']Ractor.make_shareable(ary)ary.frozen? #=> trueary[0].frozen? #=> true — recursively frozen
Copy vs. Move
When an unshareable object is sent, Ruby has to do something with it. By default it copies it via deep clone. This is safe but potentially expensive:
data = ['foo'.dup, 'bar'.freeze]r = Ractor.new do d = Ractor.receive # d[0] is a copy; d[1] is the same object (it was shareable)endr.send(data)r.join
Alternatively, move: true transfers ownership of the object to the receiving ractor, invalidating it in the sending ractor:
data = ['foo', 'bar']r = Ractor.new do d = Ractor.receive puts d.inspectendr.send(data, move: true)r.joindata.inspect# Ractor::MovedError: can not send any methods to a moved object
⚠️ Caution
Ractor::Port — The Explicit Port System
Ruby introduced Ractor::Port as the first-class communication channel. A port belongs to the ractor that creates it; only that ractor can receive from it and close it. Any ractor can send messages to it:
port = Ractor::Port.newworker = Ractor.new(port) do |p| p.send("worker result")endresult = port.receiveputs result #=> "worker result"worker.joinport.close
Ports also enable the fan-in pattern: multiple ractors sending to the same port, with the main ractor receiving results as they arrive.
Ractor.select — Waiting on Multiple Sources
Like Go’s channel select, Ractor.select blocks until any of the given ports or ractors has something available:
p1, p2 = Ractor::Port.new, Ractor::Port.newRactor.new(p1) { |p| sleep 0.1; p.send("from p1") }Ractor.new(p2) { |p| sleep 0.2; p.send("from p2") }2.times do source, value = Ractor.select(p1, p2) puts "Received #{value} from #{source.inspect}"end
Accessing Constants and Class Variables
There is an important gotcha here. Classes and modules are shareable objects — their definitions are shared among all ractors. However, accessing instance variables of classes from non-main ractors is restricted:
class Config class << self attr_accessor :value endendConfig.value = "mutable".dupRactor.new do Config.value # RuntimeError: can not access instance variables # of classes/modules from non-main Ractorsend.join
Constants are only accessible from non-main ractors if the value they point to is shareable:
GOOD = 'constant'.freeze # shareable ✓BAD = 'constant'.dup # unshareable ✗Ractor.new { puts GOOD }.join # OKRactor.new { puts BAD }.join# NameError: can not access non-shareable objects in constant BAD
💡 Recommended pattern
Ractors vs. Threads
Note that inside a Ractor you can create threads: they share the GVL with the other threads of that ractor, but run in parallel with threads of other ractors.
When (and When Not) to Use Ractors
Good candidates for Ractors:
- Batch data processing where each unit is independent
- Computation-intensive workers (compression, encryption, large-scale parsing)
- Actor-model transformation pipelines
- Servers handling fully isolated connections
Situations where threads remain preferable:
- Code that uses gems with non-thread-safe C extensions
- I/O-bound operations where the GVL is already released naturally
- Rails applications with complex shared in-memory state
⚠️ Current status — Experimental
Conclusion
Ractors represent the most significant architectural change to Ruby’s execution model in decades. They are not a universal solution — the overhead of copying objects, incompatibility with legacy C gems, and constant restrictions all require thinking about design differently.
But for workloads that genuinely need CPU-level parallelism, Ractors offer something that CRuby threads never could: running Ruby code across all your cores, with runtime isolation guarantees that make an entire family of concurrency bugs impossible.
The price is discipline in data design. The reward can be throughput that scales linearly with the number of available cores.

Ruby 3.x · Concurrency · Parallelism · Ractors · GVL · Performance
Source: CRuby reference implementation of Ractor (ractor.rb). Reproduced for educational purposes. · RubyStackNews.com