Introduction to Ruby EventMachine
Ruby EventMachine is a fast, event-driven I/O library designed to simplify building highly concurrent networked applications in Ruby. Instead of relying on threads for concurrency, EventMachine uses a single event loop that manages many connections and timers efficiently, making it well-suited for servers, daemons, proxies, and real-time applications.
The power of EventMachine becomes clear through practical code snippets: small, focused examples that illustrate patterns for timers, TCP and UDP connections, HTTP requests, and more. Understanding these patterns helps you design non-blocking applications that scale gracefully under load.
Getting Started with the Event Loop
At the heart of every EventMachine-based script is the event loop. You start it with a single call and register callbacks that describe how the application should react to events such as connections, incoming data, and timeouts.
require 'eventmachine'
EM.run do
puts "EventMachine loop started"
EM.add_timer(5) do
puts "Shutting down after 5 seconds"
EM.stop
end
end
In this snippet, EM.run starts the reactor loop. The timer schedules a callback to run five seconds later, then stops the loop. This pattern underpins most EventMachine applications: initialize state, register events, let the reactor drive the flow.
Using Timers and Periodic Tasks
Timers are one of the most common tools in EventMachine, especially for tasks like housekeeping, monitoring, or recurring jobs. There are two primary forms: one-shot timers and periodic timers.
One-Shot Timers
EM.run do
EM.add_timer(2) do
puts "This runs once after 2 seconds"
end
end
EM.add_timer schedules a single callback after the specified number of seconds. It is ideal for implementing timeouts, delayed actions, or staged workflows.
Periodic Timers
EM.run do
EM.add_periodic_timer(1) do
puts "Heartbeat: #{Time.now}"
end
end
Periodic timers fire repeatedly at a fixed interval. They are particularly useful for health checks, scheduled polling of external services, or broadcasting status updates to clients.
Basic TCP Server with EventMachine
One of the classic use cases for EventMachine is writing TCP servers that handle many connections concurrently without resorting to complex threading logic. You define a module that implements connection callbacks and pass it to EM.start_server.
require 'eventmachine'
module EchoServer
def post_init
send_data "Welcome to the Echo Server!\n"
end
def receive_data(data)
send_data "You said: #{data}"
end
def unbind
puts "Client disconnected"
end
end
EM.run do
EM.start_server('0.0.0.0', 8081, EchoServer)
puts "Echo server running on port 8081"
end
This snippet shows three fundamental callbacks:
post_init: runs right after the connection is established.receive_data: fired whenever data arrives from a client.unbind: triggered when the connection closes.
This pattern generalizes to many TCP services: authentication servers, custom protocols, proxies, and internal microservices.
Creating a TCP Client
EventMachine makes it simple to write asynchronous TCP clients as well. The client connects to a remote host and handles events in the same callback-driven style.
require 'eventmachine'
module SimpleClient
def post_init
send_data "Hello, server!\n"
end
def receive_data(data)
puts "Server says: #{data}"
close_connection_after_writing
end
def unbind
puts "Connection closed"
EM.stop
end
end
EM.run do
EM.connect('localhost', 8081, SimpleClient)
end
This code connects to the previously defined echo server, sends an initial message, then closes once it receives a response. The callback pattern keeps the client responsive even while waiting on network I/O.
Handling UDP with EventMachine
UDP is connectionless and lightweight, ideal for scenarios such as log shipping, game state updates, or metrics aggregation. EventMachine includes support for binding UDP sockets and reacting to datagrams as they arrive.
require 'eventmachine'
module UdpServer
def receive_data(data, addr)
host, port = addr[3], addr[1]
puts "Received from #{host}:#{port} - #{data.inspect}"
end
end
EM.run do
EM.open_datagram_socket('0.0.0.0', 9000, UdpServer)
puts "UDP server listening on port 9000"
end
Because UDP is connectionless, there is no post_init or unbind in the same sense as TCP. Instead, you focus on handling each individual packet.
Scheduling Defer and Blocking Work
While EventMachine encourages non-blocking operations, sometimes you must call a blocking API or perform CPU-heavy work. To avoid freezing the reactor, you can offload work using EM.defer.
EM.run do
EM.defer(
proc do
# Simulate a long-running operation
sleep 3
"Result from background task"
end,
proc do |result|
puts result
EM.stop
end
)
end
The first proc runs in a thread pool, and the second receives the result back in the main event loop. This pattern allows your application to remain responsive while still handling tasks that cannot be easily made non-blocking.
Building HTTP Clients and Servers
Although dedicated HTTP libraries exist, EventMachine also supports HTTP-oriented development, especially when you need tight control over connection behavior or want to integrate HTTP with other protocols inside the same reactor.
Simple HTTP Client Pattern
require 'eventmachine'
require 'em-http-request'
EM.run do
http = EM::HttpRequest.new('http://example.com').get
http.callback do
puts "Status: #{http.response_header.status}"
puts "Body length: #{http.response.bytesize}"
EM.stop
end
http.errback do
puts "HTTP request failed"
EM.stop
end
end
Using em-http-request, you can issue asynchronous HTTP requests that integrate seamlessly with the reactor, avoiding blocking while waiting for remote responses.
Lightweight HTTP Server Concept
You can adapt TCP server patterns to parse HTTP requests manually or use helper components that provide higher-level abstractions. This approach can be valuable for building custom APIs, lightweight dashboards, or specialized proxies where you want full control over socket behavior within EventMachine.
Graceful Shutdown and Resource Management
Robust EventMachine applications must handle shutdown cleanly to avoid dropped connections, partial writes, or inconsistent state. Typical patterns include tracking active connections and coordinating an orderly stop of the reactor.
require 'eventmachine'
module ManagedConnection
def post_init
(@@connections ||= []) << self
end
def unbind
@@connections.delete(self) if defined?(@@connections)
end
def self.connections
@@connections || []
end
end
EM.run do
EM.start_server('0.0.0.0', 8082, ManagedConnection)
trap('INT') do
puts "Stopping gracefully..."
ManagedConnection.connections.each do |conn|
conn.close_connection_after_writing
end
EM.add_timer(2) { EM.stop }
end
end
This snippet demonstrates how you can trap signals, close connections thoughtfully, and allow a brief window for in-flight data to be sent before stopping the reactor.
Common Patterns and Best Practices
Certain best practices emerge repeatedly when working with EventMachine code snippets:
- Avoid blocking calls: Use non-blocking libraries or offload work with
EM.defer. - Use callbacks consistently: Group connection logic in modules to keep behavior clear and testable.
- Leverage timers: Implement heartbeats, timeouts, and periodic jobs using
add_timerandadd_periodic_timer. - Monitor resource usage: Track open connections and memory usage in long-running daemons.
- Structure configuration: Keep ports, hosts, and credentials in configuration rather than hard-coding them.
Testing and Debugging EventMachine Code
Testing asynchronous code can be challenging, but focused snippets make it easier. Wrap your tests inside EM.run, set timeouts to avoid hanging, and assert behavior inside callbacks.
require 'minitest/autorun'
require 'eventmachine'
class EchoTest < Minitest::Test
def test_echo_response
EM.run do
EM.connect('localhost', 8081, Module.new do
define_method(:post_init) { send_data "ping\n" }
define_method(:receive_data) do |data|
begin
assert_equal "You said: ping\n", data
ensure
EM.stop
end
end
end)
EM.add_timer(2) do
flunk "Timeout waiting for echo"
EM.stop
end
end
end
end
For debugging, strategically placed logging can reveal flow through callbacks. Because EventMachine is single-threaded at the reactor level, logs remain relatively ordered, which helps trace event sequences and timing issues.
Combining Multiple Protocols in One Reactor
A key advantage of EventMachine is the ability to run different protocol handlers inside the same reactor loop. You might run a TCP server, a UDP listener, periodic timers, and outbound HTTP clients all together, sharing state as needed.
EM.run do
EM.start_server('0.0.0.0', 7000, EchoServer)
EM.open_datagram_socket('0.0.0.0', 7001, UdpServer)
EM.add_periodic_timer(10) do
puts "System check at #{Time.now}"
end
end
This unified event loop makes building complex networked systems more approachable, with a consistent model across protocols.
When to Use EventMachine
EventMachine is most valuable when your application spends a significant amount of time waiting on I/O: accepting socket connections, reading or writing over the network, handling HTTP calls, or broadcasting messages to many subscribers. In these scenarios, its event-driven design yields high concurrency with relatively modest resource usage.
If your workload is primarily CPU-bound, you may need to augment EventMachine with worker processes or consider other concurrency approaches. However, for network services, message gateways, proxies, and real-time dashboards, EventMachine code snippets provide a solid foundation on which to build.
Conclusion
Ruby EventMachine offers a compact yet powerful toolkit for asynchronous programming. By mastering a set of core code snippets—starting the reactor, working with timers, implementing TCP and UDP handlers, integrating HTTP, and managing shutdown—you can design scalable, responsive services that handle substantial traffic gracefully.
These patterns are modular and composable, allowing you to grow from small experiments to production-grade daemons without changing paradigms. Start with simple examples, expand them incrementally, and gradually assemble a library of reusable EventMachine snippets tailored to your own applications.