Skip to content
AgileLogics
6 min read

Ractors and Fibers

Prerequisites: Thread safety: Mutex and Queue

Threads cover most concurrency needs in Ruby. Two more tools handle the cases they don't: Ractors for CPU-bound parallelism, and fibers for very large numbers of concurrent I/O tasks.

Ractors — parallel Ruby code

A Ractor ("Ruby actor") is an isolated unit of execution with its own lock, so several Ractors can run Ruby code in parallel on different cores. The price is isolation: Ractors can't share mutable objects. They communicate by sending messages.

ractors = [1..2_000_000, 2_000_001..4_000_000].map do |range|
  Ractor.new(range) { |r| r.sum { |n| Math.sqrt(n) }.round }
end

p ractors.map(&:take).sum
sums.rb:2: warning: Ractor is experimental, and the behavior may change in future versions of Ruby! Also there are many implementation issues.
5333334333
  • Arguments passed to Ractor.new are copied (or moved) into the Ractor. The block can't use variables from outside it — everything it needs must be passed in as an argument.
  • take waits for the Ractor's result. (Newer Ruby versions are reworking this API — Ruby 3.5 and later use value instead of take — so check the documentation for your version.)
  • Ractors are still experimental in Ruby 3.4, and many gems aren't Ractor-safe yet.

For production CPU-bound work today, the proven approach is still multiple processes — for example, a job queue with several worker processes, or a pre-forking server like Puma in clustered mode.

Fibers — lightweight cooperative tasks

A fiber is a unit of work that pauses and resumes explicitly, rather than being switched by the system like a thread:

fiber = Fiber.new do
  puts "step 1"
  Fiber.yield
  puts "step 2"
end

fiber.resume
puts "in between"
fiber.resume
step 1
in between
step 2

Fibers are very cheap — you can have tens of thousands. Since Ruby 3.0, a fiber scheduler can pause a fiber automatically whenever it waits for I/O and run another one. You don't usually write a scheduler yourself; the async gem provides one:

require "async"
require "net/http"

Async do |task|
  %w[https://example.com https://ruby-lang.org].each do |url|
    task.async { puts Net::HTTP.get_response(URI(url)).code }
  end
end

Each request runs in its own fiber, and ordinary Net::HTTP code becomes non-blocking with no changes.

Choosing

Need Use
A handful of concurrent I/O tasks Threads
Thousands of concurrent connections Fibers with async
CPU-heavy work across cores (production) Multiple processes
CPU-heavy work across cores (experimental) Ractors