Skip to content

Ruby Ractors — Parallel Execution Without the GIL with Ractor Class

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Ruby Ractors. We cover key concepts, practical examples, and best practices to help you master this topic.

Ruby Ractors provide true parallel execution without the GIL using isolated actors communicating through message passing with Ractor class, select, and receive.

What You'll Learn

  • Creating Ractors with Ractor.new
  • Ractor communication: send and receive
  • Ractor isolation rules
  • Ractor.select for multiplexing

Why It Matters

Ractors bypass the GIL for CPU-bound parallelism. Ruby 3x3 aims for 3x faster Ruby. DodaZIP uses Ractors for parallel file compression and batch processing.

Real-World Use

Parallel data processing, CPU-intensive computations, image processing, parallel test runners, scientific computing.

flowchart LR
    A["Ractors"] --> B["Creation"]
    B --> C["Message Passing"]
    C --> D["Select"]
    D --> E["Isolation Rules"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Creating a Ractor

ractor = Ractor.new do
  Ractor.yield 42
end

puts ractor.take
# 42

Message Passing

worker = Ractor.new do
  while msg = Ractor.receive
    result = msg * 2
    Ractor.yield result
  end
end

worker.send(10)
worker.send(20)
worker.send(30)

puts worker.take
puts worker.take
puts worker.take
# 20
# 40
# 60

Parallel Computation

def expensive_compute(n)
  sleep 0.1
  n * n
end

ractors = 5.times.map do |i|
  Ractor.new(i) do |arg|
    expensive_compute(arg)
  end
end

results = ractors.map(&:take)
puts results.inspect
# [0, 1, 4, 9, 16] (in parallel)

Ractor Pool

POOL_SIZE = 4
pool = POOL_SIZE.times.map do
  Ractor.new do
    loop do
      job = Ractor.receive
      Ractor.yield job * 2
    end
  end
end

jobs = (1..10).to_a
results = jobs.map do |job|
  pool[job % POOL_SIZE].send(job)
end

puts results.map { pool.map(&:take) }.flatten.sort.inspect

Ractor.select

slow = Ractor.new do
  sleep 1
  "slow result"
end

fast = Ractor.new do
  sleep 0.1
  "fast result"
end

r, val = Ractor.select(slow, fast)
puts "First: #{val} from #{r}"
# First: fast result from #<Ractor:#2>

Ractor Server

server = Ractor.new do
  state = {}
  loop do
    action = Ractor.receive
    case action[:cmd]
    when :set
      state[action[:key]] = action[:val]
      Ractor.yield :ok
    when :get
      Ractor.yield state[action[:key]]
    end
  end
end

server.send({ cmd: :set, key: :name, val: "Ruby" })
puts server.take
server.send({ cmd: :get, key: :name })
puts server.take
# ok
# Ruby

Common Mistakes

1. Sharing Mutable Objects

ary = []
Ractor.new { ary << 1 }  # Can't share
# Use Ractor.make_shareable(ary) or Marshal.dump

2. Accessing Global State

$global = 42
Ractor.new { puts $global }  # Can't access

3. Ractor Receive Blocking

ractor = Ractor.new { Ractor.receive }  # Blocks forever if nothing sent

4. Not Handling Ractor::ClosedError

r = Ractor.new { Ractor.yield 42 }
r.close
r.take  # Ractor::ClosedError

5. Using Thread Primitives

Ractor.new { Mutex.new }  # ok, but Mutex inside Ractor is per-Ractor

Practice Questions

1. How does Ractor achieve parallelism? Each Ractor runs in a separate OS thread without the GIL. They communicate via message passing, not shared state.

2. What is Ractor.make_shareable? Deep-freezes an object so it can be safely shared between Ractors. Raises if the object references non-shareable objects.

3. How does Ractor.select work? Waits for the first Ractor to yield a value. Returns the Ractor and its value. Non-deterministic ordering.

4. What objects are shareable? Immutable objects (integers, symbols, frozen strings), Ractors, and deeply frozen objects.

Challenge: Build a parallel map function using Ractors that applies a block to each element concurrently.

Solution
def parallel_map(array, &block)
  ractors = array.map do |elem|
    Ractor.new(elem) { |e| block.call(e) }
  end
  ractors.map(&:take)
end

puts parallel_map([1, 2, 3, 4, 5]) { |n| n * n }.inspect

FAQ

{{< faq question="Do Ractors replace Threads?" >}} No. Ractors for CPU-bound parallelism. Threads for I/O-bound concurrency. They complement each other. A Ractor can contain multiple threads. {{< /faq >}}

{{< faq question="Can I share a database connection across Ractors?" >}} No, connections are not shareable. Each Ractor needs its own connection or use a Connection Pool inside the Ractor. {{< /faq >}}

{{< faq question="What is the performance benefit?" >}} CPU-bound operations can see near-linear speedup up to the number of CPU cores. I/O-bound tasks may not benefit significantly. {{< /faq >}}

{{< faq question="Can Ractors nest?" >}} Yes, a Ractor can create other Ractors. Parent Ractors can communicate with child Ractors via message passing. {{< /faq >}}

{{< faq question="Is Ractor stable in production?" >}} Ractors are production-ready in Ruby 3.1+. The API is stable but the ecosystem adoption is growing gradually. {{< /faq >}}

Try It Yourself

puts "Starting #{Ractor.count} Ractors..."
r = Ractor.new do
  result = (1..1000000).sum
  Ractor.yield result
end

puts "Computed: #{r.take}"

Expected output:

Starting 2 Ractors...
Computed: 500000500000

What's Next

Now that you understand Ractors, learn about Ruby Gems for packaging and sharing your code.

Topic Description Link
Ruby Gems Creating and publishing gems {{< ref "43-gems" >}}
Ruby Bundler Dependency management {{< ref "44-bundler" >}}
Go Goroutines Compare with Go's approach Go

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro