rubyguides

DRb distributed Ruby: building networked Ruby objects

DRb (Distributed Ruby) is a standard-library feature that lets Ruby processes communicate over a network. Whether you need to spread workload across machines or build a simple client-server system, DRb gives you a direct way to expose Ruby objects without adding extra gems.

This guide walks through DRb distributed Ruby from setup to common pitfalls. By the end, you should know how to start a server, connect a client, and decide when you want a remote reference versus a copied value.

Key takeaways

  • DRb lets one Ruby process call methods on objects owned by another Ruby process.
  • Clients usually talk to a server through a URI such as druby://localhost:8787.
  • Regular return values are marshalled, so the client gets a copy.
  • DRb::DRbUndumped keeps an object on the server and lets the client call methods on it remotely.
  • DRb is convenient for trusted environments, but it does not add authentication by default.

Those points cover the practical decisions most developers need to make before they write any DRb code. The rest of the guide adds examples that show those choices in context.

What is DRb?

Think of DRb as a small object bridge. One process exposes an object, and another process connects to it through a URI. The client can call methods on that object almost as if it were local, but the work happens on the server.

That model is easy to start with because the API stays close to normal Ruby method calls. The tradeoff is that you need to stay aware of what crosses the network and what stays local. A copied array behaves differently from a remote object, and that difference matters as soon as state changes enter the picture.

TL;DR

  • DRb lets one Ruby process call methods on objects owned by another Ruby process.
  • Clients usually talk to a server through a URI such as druby://localhost:8787.
  • Regular return values are marshalled, so the client gets a copy.
  • DRb::DRbUndumped keeps an object on the server and lets the client call methods on it remotely.
  • DRb works best in trusted environments where you control both ends of the connection.

How does DRb work?

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. The client starts its own DRb service, creates a stub object from the server’s URI, and then calls methods on that stub as if the remote object were local. The client does not need to know anything about the internal implementation of TimeServer. It just calls methods and receives marshalled return values:

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.

Should you pass objects by value or 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 is marshalled (serialized into bytes and copied). This means the client receives a copy, not a reference. The method runs on the server, the return value is serialized and sent across the wire, and the client deserializes it into a new object. Any modifications the client makes to that copy do not affect the server’s data at all:

# 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"]

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. The write method actually runs on the server, writing to the file there. The client just triggers the execution on the remote object:

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

Working with blocks

DRb supports blocks in method calls, but there is an important detail: blocks execute locally in the client process, not remotely on the server. The server yields values across the connection, but the block body runs wherever the method call was made. This works because the block itself is a closure that cannot be marshalled across the network:

# Server
class Counter
  def each
    5.times { |i| yield(i) }
  end
end

DRb.start_service("druby://localhost:8787", Counter.new)
DRb.thread.join

The client code calls each on the remote counter, and the server yields each value back to the client process. The block { |n| puts "Value: #{n}" } executes locally, printing the values on the client side. This design choice keeps blocks simple to reason about because they always run where they are defined, and it avoids the impossible problem of serializing executable code across processes:

# 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

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 call dangerous methods like instance_eval to run arbitrary code on the server. This is a critical security consideration for any DRb setup that accepts connections from outside the local machine:

# 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 do not specify a URI, DRb binds to a random port on localhost. This is useful for quick experiments or when you need a throwaway server and do not care about the exact port number. The assigned port is visible through DRb.uri:

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. This is faster than TCP because it skips the network stack entirely and communicates through the filesystem. The URI scheme changes from druby:// to drbunix://, but the rest of the API stays the same. Use the socket path as you would a host:port pair, and make sure the directory containing the socket file is writable by both processes:

require 'drb/unix'

DRb.start_service("drbunix:///tmp/myapp.sock", MyObject.new)

Common gotchas

DRb distributed Ruby is usually a good fit when every process is under your control and the goal is to share a small number of live objects. It is less attractive when you need hardened transport security, strong authentication, or a public-facing API. In those cases, a more explicit network protocol or a dedicated service layer is usually easier to secure and monitor.

One simple pattern is to keep the server object narrow. Expose just the methods the client actually needs, and keep the state changes inside that object. That makes the distributed boundary easier to test, and it also keeps the remote API from growing into an accidental grab bag.

Another useful habit is to separate transport from business logic. DRb can move method calls across processes, but your application still needs ordinary Ruby classes and methods that do the real work. If you keep those pieces separate, you can swap the transport later without rewriting your core logic.

  • 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.

Frequently asked questions

When should I use DRb?

Use DRb when you want quick process-to-process communication in Ruby and all participants are trusted. It is especially handy for small internal tools, automation tasks, or local service objects.

When should I avoid DRb?

Avoid DRb when you need public APIs, strong authentication, or a protocol that non-Ruby clients can use easily. In those cases, a web API or a message queue is usually a better fit.

Does DRb copy objects automatically?

Simple return values are copied by default. If you need the client to keep talking to the same server-side object, mark it with DRb::DRbUndumped.

See Also