Fiber Basics in Ruby

· 7 min read · Updated March 29, 2026 · intermediate
ruby fibers concurrency

Fibers sit at an interesting point in Ruby’s standard library. They are not as well known as classes like Hash or Array, but they show up in places you might not expect, particularly in iterator implementations inside Ruby’s standard library itself. Understanding fibers gives you a new mental model for control flow, one that sits somewhere between regular method calls and full threads.

This article covers how fibers work, how to create them, the resume and yield mechanics, and the producer-consumer pattern that represents their most practical use. You will also see how Fiber#transfer in Ruby 3.x adds a second way to hand control between fibers.

What Is a Fiber?

A fiber is a resumable execution context. It is a block of code that you can pause at specific points and later resume from exactly where it left off. This is cooperative multitasking — a fiber must explicitly hand control back before any other fiber can run.

This is the critical distinction from threads. Threads are preemptively scheduled by the operating system; Ruby has no say in when a thread switches context. Fibers are the opposite: Ruby switches nothing for you. You decide exactly when to pause and who runs next. That makes fiber behavior deterministic.

f = Fiber.new do
  puts "fiber started"
  Fiber.yield
  puts "fiber resumed"
end

puts "before resume"
f.resume
puts "after yield"
f.resume
puts "after second resume"

Output:

before resume
fiber started
after yield
fiber resumed
after second resume

The block passed to Fiber.new does not run immediately. It runs only when you call f.resume for the first time. When the fiber hits Fiber.yield, execution pauses and control returns to the line after f.resume. The second f.resume picks the fiber back up right after that Fiber.yield call.

Fiber.new, Resume, and Yield

The three core operations are:

  • Fiber.new { block } — creates the fiber. The block does not execute at this point.
  • f.resume — on the first call, starts the fiber. On subsequent calls, resumes it from the last Fiber.yield.
  • Fiber.yield — pauses the current fiber and returns control to whoever called resume.

Here is a slightly more complete example showing how arguments flow in both directions:

greeter = Fiber.new do |name|
  message = "Hello, #{name}"
  reply = Fiber.yield(message)
  "You said: #{reply}"
end

response = greeter.resume("Alice")
puts response  # => "Hello, Alice"

final = greeter.resume("Bob")
puts final     # => "You said: Bob"

The first resume passes "Alice" into the fiber as the block parameter. Fiber.yield suspends the fiber and sends "Hello, Alice" back to the caller. The second resume passes "Bob" as the return value of yield, which gets assigned to reply inside the fiber.

Each resume call drives the fiber forward exactly one yield step. After a fiber runs off the end of its block, subsequent calls to resume raise a FiberError.

The Producer-Consumer Pattern

The most practical use of fibers is the producer-consumer pattern. One fiber produces values, the other consumes them. They alternate by calling resume on each other.

producer = Fiber.new do
  (1..5).each do |n|
    Fiber.yield(n)
  end
  Fiber.yield(nil)  # signals end
end

consumer = Fiber.new do |source|
  while (value = source.resume)
    puts "Processed: #{value}"
  end
end

consumer.resume(producer)

Output:

Processed: 1
Processed: 2
Processed: 3
Processed: 4
Processed: 5

The producer yields each number in turn. The consumer calls source.resume to get the next value, processes it, then loops back for more. When the producer finally yields nil, the consumer’s while loop exits.

This pattern appears inside Ruby’s standard library too. The Fiber class itself powers some enumerator behavior, though you usually do not see that directly. Using it explicitly like this is useful when you have a pipeline of transformations where each stage needs to control when the previous stage advances.

Fiber.current

Fiber.current returns the Fiber instance representing whichever fiber is currently executing:

puts Fiber.current  # => main fiber (the top-level fiber)

f = Fiber.new { Fiber.current }
puts f.resume       # => the Fiber instance for f

In Ruby 3.x this becomes more important when combined with Fiber#transfer, which lets a fiber explicitly hand control to a specific other fiber rather than back to the caller of resume.

Fiber#transfer (Ruby 3.x)

Fiber#transfer adds a different control-handoff mechanism. Where resume/yield always returns control to the caller of resume, transfer lets a fiber hand control directly to any other fiber:

f1 = Fiber.new { puts "A"; Fiber.transfer; puts "C" }
f2 = Fiber.new { puts "B"; Fiber.transfer; puts "D" }

f1.transfer  # prints "A", transfers to f2
f2.transfer  # prints "B", transfers back to f1
f1.transfer  # prints "C", transfers to f2
f2.transfer  # prints "D"

Output:

A
B
C
D

With transfer, you build a dispatcher loop that explicitly picks which fiber runs next. This is useful when you have multiple fibers that need to cooperate without the call stack unwinding back to a single caller.

There is one rule that catches people: you cannot mix resume/yield with transfer on the same fiber. If a fiber has ever called transfer, all future control operations on that fiber must use transfer. Mixing them raises a FiberError.

Fibers vs Threads

The distinction matters in practice.

Threads are preemptively scheduled. The operating system can interrupt a thread at any point, so you need mutexes, semaphores, and other synchronization primitives to avoid race conditions. Ruby MRI also has a Global VM Lock (GVL), which means CPU-bound threads do not truly run in parallel, but threads still interleave during I/O operations.

Fibers give you full control over scheduling. No fiber runs unless you explicitly yield or transfer. This makes fibers ideal for iterator-style patterns and explicit producer-consumer pipelines. The tradeoff is that if a fiber enters a long computation without yielding, it will starve any other fiber waiting to run.

Use threads when you want actual parallelism or when you need to wait on external I/O without blocking your whole program. Use fibers when you want to orchestrate a pipeline of steps where each step decides when the next one runs.

Fibers vs Procs and Lambdas

Procs and lambdas are callable objects. They take arguments and return a value. Each call starts fresh — there is no state carried between calls beyond what is captured in the closure.

doubler = ->(x) { x * 2 }
doubler.call(5)   # => 10
doubler.call(7)   # => 14

A fiber maintains state across calls because execution resumes at the exact point where it paused:

counter = Fiber.new do
  n = 0
  loop do
    Fiber.yield(n)
    n += 1
  end
end

counter.resume  # => 0
counter.resume  # => 1
counter.resume  # => 2

Each resume continues from the line after Fiber.yield, carrying the updated value of n. A proc cannot do this — it would need to track n outside itself, and the loop would restart from the top each call.

Common Mistakes and Gotchas

Forgetting that the block does not run on Fiber.new. This trips people up regularly. Fiber.new { puts "hello" } does not print anything. You need f.resume.

Mixing resume and transfer on the same fibers. Once a fiber calls transfer, all fibers involved in that chain must use transfer for all future handoffs.

Resuming a fiber that has already completed. After a fiber runs off the end of its block, calling resume raises FiberError. Wrap in a rescue if you are building something that checks fiber status.

Assuming fibers run in parallel. They do not. Only one fiber executes at a time, and only when its owner fiber explicitly yields or transfers.

See Also

  • Ruby Threads Basics — learn about preemptive threading in Ruby and how it differs from the cooperative model fibers use
  • Ruby Control Flow — understand how return, break, and exceptions interact with Ruby’s control mechanisms
  • Ruby Procs and Lambdas — callable objects and closures, the foundation that fibers build on top of