DRb — Distributed Ruby
DRb (Distributed Ruby) is a powerful feature in Ruby’s standard library that lets you create applications where different Ruby processes communicate over a network. Whether you need to spread workload across machines or build a simple client-server system, DRb provides a clean way to do it without external dependencies.
This guide walks you through setting up DRb servers and clients, understanding how objects are passed between processes, and avoiding common pitfalls.
How DRb Works
DRb creates a distributed object system where one Ruby process (the server) exposes objects that another process (the client) can call methods on. Under the hood, DRb uses marshalling to serialize method calls and responses over TCP sockets.
The key things to understand upfront:
- Server: A Ruby process that exposes objects via a URI (like
druby://localhost:8787) - Client: A separate process that creates a “stub” pointing to the server’s URI
- Communication: Method calls are serialized, sent to the server, executed there, and results are returned
DRb is part of Ruby’s standard library, so you don’t need to install anything:
require 'drb/drb'
Building Your First DRb Server
A minimal DRb server needs three things: a URI to listen on, an object to expose (called the “front object”), and code to keep the server running.
Create a file called time_server.rb:
require 'drb/drb'
URI = "druby://localhost:8787"
class TimeServer
def get_current_time
Time.now
end
def get_server_info
"DRb Time Server v1.0"
end
end
FRONT_OBJECT = TimeServer.new
DRb.start_service(URI, FRONT_OBJECT)
puts "DRb server started at #{URI}"
DRb.thread.join
The DRb.thread.join line keeps the main thread alive so the server doesn’t exit immediately. Without it, the process would start the server and then immediately quit.
Connecting a Client
Now create a client to call methods on that server. Save this as time_client.rb:
require 'drb/drb'
SERVER_URI = "druby://localhost:8787"
# Start service (needed for callbacks; harmless if not used)
DRb.start_service
# Create a stub for the remote object
timeserver = DRbObject.new_with_uri(SERVER_URI)
# Call methods as if the object was local
puts "Server says: #{timeserver.get_server_info}"
puts "Current time: #{timeserver.get_current_time}"
Run the server in one terminal, then run the client in another:
ruby time_server.rb
ruby time_client.rb
You should see output showing the server’s response. The client doesn’t need to know the internal implementation of TimeServer — it just calls methods and receives results.
Passing Objects: By Value vs. By Reference
This is the most important concept in DRb, and it’s where most people get confused.
Marshalling (Pass by Value)
By default, when you return an object from a DRb method, it’s marshalled (serialized into bytes and copied). This means the client receives a copy, not a reference.
# Server
class Inventory
def get_items
["apple", "banana", "cherry"]
end
end
# Client receives a COPY of the array
items = inventory.get_items # => ["apple", "banana", "cherry"]
The method runs on the server, returns a copy, and any modifications on the client don’t affect the server’s data.
DRbUndumped (Pass by Reference)
Sometimes you want the client to hold a reference to an object, so method calls execute on the server. Include DRb::DRbUndumped in your class to enable this:
require 'drb/drb'
class Logger
include DRb::DRbUndumped # Important!
def initialize(filename)
@filename = filename
end
def write(message)
File.open(@filename, "a") { |f| f.puts "#{Time.now}: #{message}" }
end
end
class LoggerFactory
def get_logger(name)
Logger.new("/tmp/#{name}.log")
end
end
DRb.start_service("druby://localhost:8787", LoggerFactory.new)
DRb.thread.join
Now the client can get a logger and call methods on it:
require 'drb/drb'
DRb.start_service
factory = DRbObject.new_with_uri("druby://localhost:8787")
logger = factory.get_logger("app")
logger.write("Application started") # Executed on the SERVER
The write method actually runs on the server, writing to the file there. The client just triggers the execution.
Working with Blocks
DRb supports blocks in method calls, but there’s a catch: blocks execute locally, not remotely. Here’s why:
# Server
class Counter
def each
5.times { |i| yield(i) }
end
end
DRb.start_service("druby://localhost:8787", Counter.new)
DRb.thread.join
# Client
counter = DRbObject.new_with_uri("druby://localhost:8787")
counter.each { |n| puts "Value: #{n}" }
# Output:
# Value: 0
# Value: 1
# Value: 2
# Value: 3
# Value: 4
The server yields values, but the block runs in the client’s process. This is because Proc objects cannot be marshalled across the network.
Security Considerations
DRb has no built-in authentication or encryption. By default, anyone who can reach your DRb URI can call methods on your objects. This is a serious concern.
The Danger of instance_eval
If you expose an object via DRb, a malicious client can do dangerous things:
# DANGEROUS: Never expose objects to untrusted clients
ro = DRbObject.new_with_uri("druby://your.server.com:8989")
ro.instance_eval("system('rm -rf *')")
Using Access Control Lists
Restrict which clients can connect using an ACL:
require 'drb/drb'
require 'drb/acl'
# Allow only localhost, deny everything else
acl = ACL.new(%w[deny all allow localhost 127.0.0.1])
DRb.start_service("druby://localhost:8787", MyObject.new, { acl: acl })
The $SAFE Variable
DRb respects Ruby’s $SAFE variable, which controls what operations are allowed:
| Level | Restriction |
|---|---|
| 0 | No restrictions (default) |
| 1 | Disables eval and taint checks |
| 2 | Disables modified globals, file writes |
| 3 | All objects become tainted |
| 4 | Disables iterators entirely |
Note that $SAFE is deprecated in modern Ruby, but it still works. For production systems, use ACLs and network isolation instead.
Binding to Random Ports
If you don’t specify a URI, DRb binds to a random port on localhost:
DRb.start_service
puts "Server URI: #{DRb.uri}" # => "druby://127.0.0.1:53787"
This is useful when you need a quick server and don’t care about the port number.
UNIX Domain Sockets
For local inter-process communication, you can use UNIX domain sockets instead of TCP:
require 'drb/unix'
DRb.start_service("drbunix:///tmp/myapp.sock", MyObject.new)
This is faster than TCP for communication between processes on the same machine.
Common Gotchas
-
Garbage collection: Without
DRb::TimerIdConv, exported objects can be garbage collected if no references exist. For long-lived servers, configure an ID conv that keeps objects alive. -
Thread safety: The DRb server runs in its own thread. Make sure your front object is thread-safe if multiple clients will connect simultaneously.
-
Main thread must wait: Always call
DRb.thread.joinor otherwise keep the main thread alive, or the process will exit immediately.
See Also
- Ruby Concurrency — Threads — Learn about Ruby’s threading model, which is useful when building multi-threaded DRb servers
- Ruby Bundler and Gems — Understanding how Ruby manages dependencies, useful when building larger distributed systems
- Ruby Net::HTTP Requests — Another way to communicate between processes, this time over HTTP