Introduction
Ractor::Port is the primary messaging primitive in Ruby 4.0's Ractor system. Ports replace the removed Ractor.yield and Ractor#take methods, providing explicit, directional communication channels between Ractors. Understanding ports is essential for building any multi-Ractor system beyond simple compute-and-return patterns.
Key Concepts
- Ractor::Port: A unidirectional message channel. Any Ractor can send to a port; any Ractor holding a reference can receive from it.
- Default Port: Every Ractor is born with one port, accessible via
ractor.default_portor internally viaRactor.receive. port.send(obj): Enqueues an object on the port. The object is deep-copied unless it is shareable or moved.port.receive: Blocks until a message arrives, then returns it.port.close: Closes the port so no more messages can be sent. Receivers getRactor::ClosedError.
Real World Context
In production pipeline architectures — such as an ETL system that extracts records, transforms them, and loads them into a database — each stage runs in its own Ractor. Ports connect the stages, providing backpressure-free message passing without shared memory. This pattern scales linearly with CPU cores.
Deep Dive
Creating and Using Ports
You create a port with Ractor::Port.new and pass it into Ractors as an argument:
rubyresult_port = Ractor::Port.new worker = Ractor.new(result_port) do |port| # Perform work inside the Ractor total = (1..100).sum # Send the result back through the port port.send(total) end # Receive the result on the main Ractor answer = result_port.receive # => 5050 worker.join
The port acts as a mailbox: worker sends a value into it, and the main Ractor pulls it out with receive. This decouples the sender from the receiver.
Default Port
Every Ractor has a default port. Sending to a Ractor with r.send(msg) is shorthand for r.default_port.send(msg). Inside the Ractor, Ractor.receive reads from the default port:
rubyworker = Ractor.new do # Ractor.receive reads from this Ractor's default port name = Ractor.receive "Hello, #{name}!" end # r.send is shorthand for r.default_port.send worker.send("Alice") worker.value # => "Hello, Alice!"
Both forms are equivalent, but explicit ports are preferred when a Ractor needs multiple independent channels.
Multiple Ports Pattern
Use separate ports for input and output to keep concerns cleanly separated:
rubyinput_port = Ractor::Port.new output_port = Ractor::Port.new processor = Ractor.new(input_port, output_port) do |inp, out| while (data = inp.receive) out.send(data.transform_keys(&:upcase)) end rescue Ractor::ClosedError # Input port was closed, stop processing end input_port.send({ name: "Alice", role: "admin" }) result = output_port.receive # => { NAME: "Alice", ROLE: "admin" } input_port.close # Signal the worker to stop processor.join
Closing the input port causes inp.receive to raise Ractor::ClosedError, which the Ractor catches to exit its loop gracefully.
The << Operator
Ractor#send has a shorthand << operator that works identically:
rubyworker = Ractor.new { Ractor.receive * 2 } worker << 21 # Same as worker.send(21) worker.value # => 42
Common Pitfalls
- Forgetting to close ports — Long-running Ractors that loop on
port.receivewill hang forever unless the port is closed. Always close input ports when you are done sending. - Sending non-shareable ports — A
Ractor::Portis itself a shareable object, so it can be passed to multiple Ractors. But forgetting to pass the port as an argument toRactor.newand trying to capture it from the closure will raiseRactor::IsolationError.
Best Practices
- One port per concern — Use a dedicated port for commands, another for results, and another for errors rather than multiplexing everything on the default port.
- Rescue
ClosedErrorin loops — TreatRactor::ClosedErroras the normal shutdown signal for worker Ractors that loop onreceive.
Summary
Ractor::Port.newcreates a message channel between Ractors.port.send(obj)enqueues;port.receivedequeues (blocking).- Every Ractor has a
default_port;r.send(msg)is shorthand forr.default_port.send(msg). - Close ports with
port.closeto signal shutdown to looping receivers. - The
<<operator is an alias forsend.
Code Examples
# Fan-out pattern: one producer, multiple consumers via ports
task_port = Ractor::Port.new
result_port = Ractor::Port.new
# Spawn 3 worker Ractors sharing the same task port
3.times do |id|
Ractor.new(task_port, result_port, id) do |tasks, results, worker_id|
while (url = tasks.receive)
# Simulate processing a URL
results.send({ worker: worker_id, url: url, status: 200 })
end
rescue Ractor::ClosedError
# Producer closed the task port, exit gracefully
end
end
# Enqueue tasks
urls = %w[/users /orders /products /inventory /reports]
urls.each { |url| task_port.send(url) }
task_port.close
# Collect results
urls.size.times { puts result_port.receive.inspect }