Non‑Blocking IO.select in Ruby: Introduction to Fiber::Scheduler#io_select

Introduction

Ruby 3.1 introduced Fiber::Scheduler#io_select, making IO.select work seamlessly with fiber-based concurrency. Before diving in, let’s clarify some key concepts.

What’s a Fiber?

A fiber is a lightweight, pausable unit of execution in Ruby. Unlike threads, fibers don’t run in parallel—they cooperatively yield control to each other, making them predictable and efficient for I/O-bound tasks.

fiber = Fiber.new do
  puts "Starting"
  Fiber.yield  # Pause here
  puts "Resuming"
end

fiber.resume  # => "Starting"
fiber.resume  # => "Resuming"

What’s a Scheduler?

A scheduler manages when fibers run. When a fiber performs I/O (like reading from a socket), the scheduler pauses it and runs other fibers instead of blocking. This enables non-blocking I/O without callbacks or threads.

Popular schedulers include the Async gem, which powers the Falcon web server.

Real-World Example

Imagine building a web scraper that fetches 100 URLs. With traditional blocking I/O, you’d wait for each request sequentially. With fibers and a scheduler, you can start all 100 requests concurrently—the scheduler switches between them as responses arrive.

require 'async'
require 'async/http/internet'

Async do
  internet = Async::HTTP::Internet.new

  urls = 100.times.map { |i| "https://example.com/page#{i}" }

  # All requests run concurrently
  responses = urls.map do |url|
    Async { internet.get(url) }
  end.map(&:wait)

  puts "Fetched #{responses.size} pages"
end

The Problem Before Ruby 3.1

IO.select is a system call that monitors multiple file descriptors (sockets, files, etc.) and returns when any become ready for reading or writing. It’s commonly used in networking code.

Before Ruby 3.1, calling IO.select inside a fiber blocked the entire thread, preventing the scheduler from managing other fibers. This broke concurrency for any library using IO.select.

# Before: blocks all fibers
IO.select([socket_a, socket_b])
# Scheduler can't run other fibers here

The Solution: io_select Hook

Ruby 3.1 added Fiber::Scheduler#io_select, allowing schedulers to handle IO.select non-blockingly. When IO.select is called, Ruby delegates to the scheduler instead of blocking.

# After: scheduler manages the wait
IO.select([socket_a, socket_b])
# Other fibers continue running

Schedulers implement this method:

def io_select(readable, writable, exceptional, timeout)
  # Non-blocking multiplexing logic
  # Returns arrays of ready descriptors
end

Practical Impact

This change means:

  • Legacy compatibility: Old libraries using IO.select now work with fiber schedulers
  • No code changes needed: Existing code becomes non-blocking automatically when a scheduler is active
  • Better performance: Applications can handle thousands of concurrent I/O operations efficiently

For example, the redis-rb gem uses IO.select internally. With this feature, it works seamlessly with fiber-based web servers like Falcon without modifications.

Why This Matters

Ruby is moving toward fiber-based concurrency as the standard for I/O-bound applications. Features like io_select ensure the entire ecosystem—including older libraries—can participate in this model without rewrites.

This gives us the best of both worlds: simple, synchronous-looking code that performs like async systems.

References

Need help on your Ruby on Rails or React project?

Join Our Newsletter