Introduction
Fibers are Ruby's primitive for cooperative (non-preemptive) concurrency. Unlike threads where the OS decides when to switch, with fibers YOU explicitly control when execution pauses and resumes. This makes fibers predictable, lightweight, and ideal for generators, coroutines, and async I/O patterns.
Key Concepts
- Fiber: A lightweight execution context with its own call stack (~4KB initial) that you manually pause and resume.
- Cooperative Scheduling: The running fiber decides when to yield control — no preemption, no race conditions from unexpected switches.
- Fiber.yield: Pauses the current fiber and returns a value to whichever code called
resume.
Real World Context
The async gem (used by Falcon web server) builds on fibers to handle thousands of concurrent connections with minimal memory. Enumerator, the backbone of Ruby's lazy iteration, is implemented with fibers internally. Every time you use each_with_object or Enumerator::Lazy, fibers are working behind the scenes.
Deep Dive
Basic Fiber Usage
A fiber is created with Fiber.new and started with resume. When the fiber calls Fiber.yield, it pauses and returns the yielded value to the caller.
rubyreport_fiber = Fiber.new do puts "Generating report..." Fiber.yield "Phase 1 complete" # Pauses here, returns to caller puts "Finalizing report..." "Report done" end status = report_fiber.resume # Prints: Generating report... puts status # => "Phase 1 complete" final = report_fiber.resume # Prints: Finalizing report... puts final # => "Report done"
The first resume runs the fiber until it hits Fiber.yield, which returns "Phase 1 complete" to the caller. The second resume continues from where the fiber paused and returns the block's final value.
Two-Way Communication
Fibers support bidirectional data passing. The first resume passes its argument as the block parameter. Subsequent resume calls pass their argument as the return value of Fiber.yield.
rubyprocessor = Fiber.new do |initial_batch| puts "Processing: #{initial_batch}" next_batch = Fiber.yield "batch 1 done" puts "Processing: #{next_batch}" "all batches complete" end result1 = processor.resume("orders-jan") # Prints: Processing: orders-jan puts result1 # => "batch 1 done" result2 = processor.resume("orders-feb") # Prints: Processing: orders-feb puts result2 # => "all batches complete"
The first resume delivers "orders-jan" as the initial_batch parameter. The second resume delivers "orders-feb" as the return value of Fiber.yield.
Fiber vs Thread Comparison
| Aspect | Thread | Fiber |
|---|---|---|
| Scheduling | Preemptive (OS decides) | Cooperative (you decide) |
| Stack size | ~1MB per thread | ~4KB initial per fiber |
| Context switch | Expensive (OS level) | Cheap (user-space) |
| Parallelism | Yes (limited by GVL) | No (single thread) |
| Race conditions | Possible | Impossible (no preemption) |
Generator Pattern
Fibers are perfect for infinite sequences. The fiber yields values one at a time, pausing between each.
rubydef prime_generator Fiber.new do candidate = 2 loop do if (2..Math.sqrt(candidate)).none? { |d| candidate % d == 0 } Fiber.yield candidate end candidate += 1 end end end primes = prime_generator 10.times { print "#{primes.resume} " } # Output: 2 3 5 7 11 13 17 19 23 29
The generator maintains its position between calls. Each resume produces the next prime without recomputing from scratch.
Common Pitfalls
- Resuming a dead fiber — Once a fiber's block finishes, calling
resumeraisesFiberError: dead fiber called. Always checkfiber.alive?before resuming if the number of yields is unknown. - Confusing Fiber.yield with block yield —
Fiber.yieldpauses the fiber and returns to the caller. Ruby'syieldkeyword calls a block. They are completely different mechanisms despite sharing the name.
Best Practices
- Use fibers for lazy producers — When you need to generate values on demand without computing the entire sequence upfront, fibers are more memory-efficient than building an array.
- Prefer Enumerator for simple generators — Ruby's
Enumerator.new { |y| ... }uses fibers internally and integrates witheach,map, and other Enumerable methods. Use raw fibers only when you need two-way communication or custom scheduling.
Summary
- Fibers are lightweight (~4KB) execution contexts with cooperative scheduling — you control when they pause and resume.
Fiber.yieldpauses a fiber and sends a value back to the caller;resumecontinues from where it paused.- Two-way communication lets callers send data into fibers on each resume.
- Fibers cannot run in parallel but eliminate race conditions because switching is explicit.
Code Examples
# Infinite Fibonacci generator using a Fiber
def fibonacci
Fiber.new do
current, next_val = 0, 1
loop do
Fiber.yield current
current, next_val = next_val, current + next_val
end
end
end
fib = fibonacci
10.times { print "#{fib.resume} " }
# Output: 0 1 1 2 3 5 8 13 21 34