Fibers and the fiber scheduler

Ruby · Expert · 16 min

The Fiber scheduler lets Ruby run thousands of concurrent I/O operations with ordinary blocking-style code.

A Fiber is a lightweight coroutine: it has its own stack, runs when you call resume, and pauses itself with Fiber.yield, handing a value back. Fibers are cooperatively scheduled, cost far less memory than threads and switch without OS involvement. External Enumerators (e.next) are implemented with fibers.

f = Fiber.new { x = Fiber.yield(1); x * 10 }
f.resume      # => 1 (paused at yield)
f.resume(5)   # => 50 (yield returned 5; fiber finished)

Ruby 3.0 added the fiber scheduler interface. When a non-blocking fiber is running and a scheduler has been set with Fiber.set_scheduler, blocking operations like socket and pipe reads, sleep, DNS lookups, Process.wait and Thread#join call hooks on the scheduler instead of blocking the thread. The scheduler parks that fiber, runs others, and resumes it when its I/O is ready, typically using epoll, kqueue or io_uring.

Ruby ships only the interface; implementations come from gems, most notably async (with the Falcon web server). With it you write normal sequential code inside Async do ... end tasks and get event-loop concurrency without callbacks or await keywords.

Limits: it only helps I/O that goes through Ruby's IO layer or scheduler-aware C extensions. CPU-bound work, or a C extension that blocks without releasing control, still stalls every fiber on that thread. Fibers don't run in parallel; combine with processes for multiple cores.

Example

gen = Fiber.new do
  3.times { |i| Fiber.yield "step #{i}" }
  "finished"
end
4.times { puts gen.resume }      # step 0, step 1, step 2, finished
p gen.alive?                     # false

# Round-robin of cooperative tasks
tasks = %w[A B].map do |name|
  Fiber.new do
    2.times { |i| puts "#{name}#{i}"; Fiber.yield }
    :done
  end
end
until tasks.empty?
  tasks.reject! { |t| t.resume == :done }
end
# A0 B0 A1 B1 (one per line)

# With the async gem (Fiber scheduler), these 3 sleeps overlap:
#   require "async"
#   Async { 3.times.map { Async { sleep 1 } }.each(&:wait) }  # ~1s total

Remember

  • Fibers are coroutines: resume runs, Fiber.yield pauses and returns a value
  • The fiber scheduler turns blocking I/O calls into event-loop switches
  • Ruby defines the interface; gems like async implement it

Watch out for

  • Expecting fibers to speed up CPU-bound work; they never run in parallel.

Check yourself

  1. With a fiber scheduler set, what happens when a non-blocking fiber calls sleep(1)?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.ruby-lang.org/en/3.4/Fiber.html

Builds on

Unlocks

Nothing builds on this yet.