DRb — Distributed Ruby

· 5 min read · Updated March 19, 2026 · intermediate
ruby distributed stdlib

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:

LevelRestriction
0No restrictions (default)
1Disables eval and taint checks
2Disables modified globals, file writes
3All objects become tainted
4Disables 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.join or otherwise keep the main thread alive, or the process will exit immediately.

See Also