Introduction

Ruby 3.x introduced a Fiber Scheduler API that modernizes async I/O by letting you hook into Fibers, lightweight concurrency primitives, to perform non-blocking operations. This means you can ditch older event-driven gems and write clean, synchronous-looking code that under the hood uses async I/O.

In this post we’ll cover:

  • How the Fiber Scheduler API works
  • Replacing EventMachine and Celluloid
  • Writing non-blocking DNS, HTTP, and DB adapters
  • Integrating with the async gem ecosystem
  • Real-world: building a mini chat server

The Fiber Scheduler API

A Fiber Scheduler intercepts blocking operations (like IO.read) and delegates them to non-blocking implementations. You register a scheduler globally:

class MyScheduler < Fiber::Scheduler
  def io_read(io, buffer, size, offset)
    # use non-blocking read under the hood
    selector = IO::Selector.new
    selector.register(io, :read)
    selector.select(1) # wait until readable
    io.read_nonblock(size, buffer)
  end

  # define io_write, etc.
end

Fiber.set_scheduler(MyScheduler.new)

# Now any IO operations run inside Fibers will use your scheduler

Ditching EventMachine and Celluloid

Before Fiber schedulers, popular async libs were EventMachine (callback-heavy) and Celluloid (actor model). With the scheduler API, you get:

  • Linear code flow instead of nested callbacks
  • Built-in integration without external runtimes
  • Better performance since it’s part of the core

Building Non-blocking Adapters

You can wrap existing sync libraries by delegating to their non-blocking counterparts. For example, a simple HTTP GET:

require 'async/http/client'
require 'async/io/ssl_socket'

Async do
  uri = URI("https://example.com")
  client = Async::HTTP::Client.new(
    Async::HTTP::Endpoint.parse(uri)
  )
  response = client.get(uri.path)
  puts response.read
end

Or roll your own with Socket#read_nonblock and your scheduler.

Integrating with async Gems

The async gem ecosystem provides high-level building blocks:

  • async-io for filesystem and sockets
  • async-http for HTTP/2 and streaming
  • async-postgres for DB connections

They all run on your scheduler automatically inside Async do ... end blocks.

Real-world: Mini Chat Server

require 'async'
require 'async/io'

Async do
  server = Async::IO::Socket.tcp('0.0.0.0', 3000)
  puts "Chat server running on port 3000"

  server.each do |client|
    Async do
      client.write "Welcome to async chat!\n"
      loop do
        message = client.readpartial(1024)
        broadcast_to_all(message)
      end
    rescue EOFError
      client.close
    end
  end
end

This code looks sync but is fully non-blocking.

Conclusion

The Fiber Scheduler API in Ruby 3.x makes async I/O feel natural and integrates directly with Ruby’s Fibers. By leveraging the async gems or writing custom schedulers, you can build high-performance services without callback hell. Give it a spin in your next project!