cool.io: evented I/O for Ruby on top of libev
Simple evented I/O for Ruby.
At a glance
- What is it?
- cool.io is a Ruby event library built on libev, so it gets the epoll system call on Linux, kqueue on the BSDs and OS X, and the completion ports interface on Solaris through one API. It provides a loop, four kinds of watcher, asynchronous wrappers around Ruby's own socket classes, and a small Sinatra-like DSL for writing event-driven servers.
- Who is it for?
- cool.io fits a Ruby developer who wants an event loop without adopting a whole web framework around it, and who needs the same program to work on Linux, the BSDs, OS X, and Solaris without choosing a different backend per platform. It does not fit a project that has already standardised on another event loop, and it does not fit a team that cannot ship a native extension, because the library is a binding to a C library rather than a pure Ruby one.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One library, four system calls
cool.io is an event library for Ruby built on the libev event library, and the reason to use libev rather than a native Ruby loop is portability without a rewrite.
The library provides a cross-platform interface to the high performance system calls of each operating system. On Linux that is the epoll system call, on the BSDs and OS X it is kqueue, and on Solaris it is the completion ports interface. A program written against cool.io therefore uses the best available mechanism on the machine it runs on, without the author writing three implementations.
On top of that, the library binds asynchronous wrappers around Ruby's core socket classes, so the ordinary Ruby socket objects can be used together with it to build asynchronous event-driven applications. The wrappers are not a separate idea: the event loop and the socket objects are the same library, which is what makes the buffering and the callbacks line up.
Including it in a program takes one line.
require 'cool.io'The project is MIT licensed, the last push was on 2026-10-20, and the repository has no tagged releases, so the version comes from the gem rather than from a release list.
The native part is visible in the repository layout. There is an extension directory for the C binding, a gem specification, a Rakefile, a change log, and two patch files whose names explain themselves: one for the Ruby global interpreter lock and one for the select implementation on Windows. Those two files are the portability story in two lines, because they are the places where libev's assumptions about the runtime do not hold on every platform the library claims to support.
The loop, the watcher, and the four kinds
The library builds on two core classes, and the division between them is the whole model.
The loop is the thing that waits. It uses the underlying high performance system calls to wait for events, and it is the object you start to make anything happen.
The watcher is the base class for event observers. Once you attach an observer to a loop and start running that loop, you begin receiving callbacks to particular methods when events occur. So a watcher is an object with methods that the library calls, and a loop is the scheduler that decides when.
There are four types of watcher, and each covers one kind of thing you might want to wait for.
The input and output watcher waits for an IO object to become readable, writable, or both. That is the socket case, and it is the one most programs use.
The timer watcher waits for a specified duration and then fires an event, and it can also be configured to fire at specified intervals, so it covers both a deadline and a periodic tick.
The stat watcher monitors files or directories for changes, which is how you react to a file appearing without polling it yourself.
The async watcher is the one that solves the awkward problem of waking up an event loop that is running in another thread. It allows each thread to run a separate loop, with the loops signalling each other, and it is what makes the library usable in a program that has real threads in it rather than a single threaded event loop.
That four-way split is a small API surface, and it is worth noticing that the library does not offer more. A watcher that waits for a condition, a watcher for processes, a watcher for signals, none of those are here, and their absence is what keeps the interface small enough to read in one sitting.
Attach, detach, disable, and enable
Watchers have five important methods, and the interesting part is the difference between unbinding and pausing.
The attach method binds a watcher to a given event loop, and if the watcher is already bound to a loop it is detached from that one first and then attached to the new one. So attaching is a move, not a copy, and the same watcher cannot be running in two loops at once.
The detach method completely unbinds a watcher from an event loop.
Then there are the two that look like the same thing and are not. The disable method stops the watcher from receiving events but does not unbind it from the loop. The enable method re-enables a watcher that has been disabled, and the watcher must still be bound to an event loop for that to work.
The documentation is explicit about which one to use when you are toggling a watcher on and off, and the reason is performance rather than semantics: disabling performs better than completely removing the watcher from the event loop. That is a useful detail, because the obvious implementation of a toggle is to detach and re-attach, and the obvious implementation is the slower one on every cycle.
The last method is the evloop accessor, which returns the loop the watcher is currently bound to. It is the way a piece of code finds out where it is running, which matters in a program that has several loops, one per thread, and a watcher that might be attached to any of them.
So the lifecycle of a watcher is: attach it, disable and enable it to pause it, detach it to move it or to remove it, and ask it for its loop when you need to know where it is attached. Five methods, and each one has a reason to exist.
The socket wrappers, and the write buffering
The asynchronous wrappers are the part of the library that touches ordinary Ruby code, and there are two classes to know about.
The TCP socket is a buffered wrapper around Ruby's core socket class, and it is used for TCP sockets. You can create outgoing connections asynchronously through its connect method.
The buffering is the point. The socket provides write buffering to ensure that writing never blocks, which is what makes it safe to call write from a callback without stalling the event loop while the kernel buffer is full.
The callbacks cover the whole life of a connection: when the connection is opened, or failed, when data is received, when the write buffer has been written out completely, and when the connection closes. Those four moments are what a protocol implementation needs, and the fact that a failed connection has its own callback rather than an exception is the idiomatic choice for this style of programming.
The TCP server is a wrapper around Ruby's own server class, and it creates new instances of the TCP socket, or of any subclass you care to supply, whenever an incoming connection arrives. That single sentence is the mechanism behind most of what you build with this library: you write one class that knows how to talk your protocol, and the server makes one of them per client.
The design is worth contrasting with the alternative. A threaded server spawns a thread per connection, which is easy to write and expensive at a few thousand connections. An event-driven one multiplexes them all through one loop, which is harder to write and cheap at that scale. This library takes the second side, and it takes it with the same socket classes the first side would use.
A Sinatra-like DSL, and the subclass alternative
The library offers a DSL in the style of Sinatra for writing event-driven programs, and the example server is short enough to be the whole story.
require 'cool.io'
require 'cool.io/dsl'
ADDR = '127.0.0.1'
PORT = 4321
cool.io.connection :echo_server_connection do
on_connect do
puts "#{remote_addr}:#{remote_port} connected"
end
on_close do
puts "#{remote_addr}:#{remote_port} disconnected"
end
on_read do |data|
write data
end
end
puts "Echo server listening on #{ADDR}:#{PORT}"
cool.io.server ADDR, PORT, :echo_server_connection
cool.io.runThe first call creates a connection class with a name and defines the callbacks for the events. The second creates the server on an address and a port, and says which connection class to use for each incoming connection. The third starts everything, and calling run blocks while the server listens.
The alternative is to subclass the socket class directly, and the library documents that too, with the same echo server written the other way.
class EchoServerConnection < Cool.io::TCPSocket
def on_connect
puts "#{remote_addr}:#{remote_port} connected"
end
def on_close
puts "#{remote_addr}:#{remote_port} disconnected"
end
def on_read(data)
write data
end
end
server = Cool.io::TCPServer.new(HOST, PORT, EchoServerConnection)
server.attach(Cool.io::Loop.default)
Cool.io::Loop.default.runThe subclass version is the one to read if you are writing a real protocol, because it is where the class boundary sits. The observer type is made by subclassing an existing one and adding new implementations of the event handlers, a new event loop is created, a server is created and attached to that loop, and the loop is started with the run method.
And that run method has a behaviour worth knowing. It blocks until there are no active watchers for the loop, or until the loop is stopped explicitly. A program that attaches nothing has nothing to wait for, so it returns immediately, which is the kind of thing that looks like a bug the first time it happens to you.
The gem, the examples, and what the repository holds
The repository is a gem with a native extension, and the layout reflects that.
There is the extension directory for the C binding, the library directory, the specification directory for the tests, the examples directory, the gem specification, a Rakefile, a change log, and a continuous integration configuration from AppVeyor, which is the older of the two build services and is a common choice for gems with a native extension that has to be built on several platforms.
The examples directory is the best guide to the library, and the file names are the features. There is a plain echo client and a plain echo server, a version of each written with the DSL, a version of the server written with callbacks, and one that talks to Google. The callbacked server is the interesting one, because it is the middle ground between the DSL and the full subclass, and it is the shape most real code ends up in.
The test configuration is a single file, which is the standard marker that the tests use the RSpec style, and the continuous integration runs on the older service, which is a hint about the age of the project and its maintenance status.
The two patch files are worth one more look, because they explain a lot about the constraints. One adapts libev to the Ruby global interpreter lock, and the other provides a select based implementation for Windows. Both are the kind of fix that has to be maintained when the runtime underneath changes, and the fact that they are kept as patches in the repository rather than upstreamed is a sign of how the library is maintained.
Taken together, the picture is of a small, careful, native-binding library for people who want an event loop in Ruby and want it to work the same way on every platform, with the ergonomics of a known gem and a very small API to learn.
Editorial conclusion
cool.io fits a Ruby developer who wants an event loop without adopting a whole web framework around it, and who needs the same program to work on Linux, the BSDs, OS X, and Solaris without choosing a different backend per platform. It does not fit a project that has already standardised on another event loop, and it does not fit a team that cannot ship a native extension, because the library is a binding to a C library rather than a pure Ruby one. Verify first that the event loop you are running has something to wait for, since the run method returns as soon as no watcher is active, that you understand the difference between disabling a watcher and detaching it, and that the socket subclass you write does not block in its callbacks, since the write buffering only covers the write path.
Frequently asked questions
What is cool.io?
It is an event library for Ruby built on libev, which gives it a cross-platform interface to the high performance system calls: epoll on Linux, kqueue on the BSDs and OS X, and completion ports on Solaris. It also binds asynchronous wrappers around Ruby's core socket classes so they can be used with the event loop.
What are the four types of watcher in cool.io?
An input and output watcher that waits for an IO object to become readable, writable, or both, a timer watcher that fires after a duration or at intervals, a stat watcher that monitors files and directories for changes, and an async watcher that wakes up an event loop running in another thread so that separate loops in separate threads can signal each other.
What is the difference between disabling and detaching a watcher?
Disabling stops the watcher from receiving events but leaves it bound to its loop, and enabling reverses that. Detaching unbinds the watcher from the loop entirely. The documentation recommends disabling and enabling rather than detaching and attaching when you are toggling a watcher, because it performs better than removing the watcher from the loop.
How does cool.io write data without blocking?
The TCP socket wrapper provides write buffering so that writing never blocks, and it reports through callbacks when the connection opens or fails, when data arrives, when the write buffer has been flushed, and when the connection closes. The TCP server creates a new instance of that socket, or of a subclass you supply, for every incoming connection.
Why does my cool.io program return immediately from run?
Because the run method blocks only until there are no active watchers left for the loop, or until the loop is stopped explicitly. A loop with nothing attached has nothing to wait for, so it returns at once, and the server has to be attached before the loop is started.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/socketry-cool-io)Was this useful?