Ruby 3.0 makes parallelism and type contracts explicit
Ruby 3.0 combines experimental Ractor isolation, fiber scheduler hooks, and separate RBS signatures. Its performance milestone applies to a specific benchmark workload.
Ruby's convenient object model makes it easy to pass a mutable object through several layers of a program. Parallel execution makes that convenience harder to preserve safely. Ruby 3.0 introduces Ractor with restrictions on what different execution units may share. Its approach requires programs to express object ownership and communication more carefully, even though ordinary Ruby code continues to use the familiar object model.
The December 25 release announcement presents Ruby 3.0.0 as the result of work on performance, concurrency, and static analysis. Those goals involve different mechanisms. Ractor enables parallel Ruby execution through isolation. Fiber scheduler hooks allow supported blocking operations to cooperate with an event loop. RBS describes program types in separate signatures. None of these mechanisms automatically converts an existing application into a parallel, statically checked program.
I find Ruby approachable because its objects and blocks give common operations a consistent expression. That preference does not remove the need to understand what a method can mutate or where execution can pause. Ruby 3.0 makes those questions more explicit in code that adopts its new concurrency facilities.
Ractor limits shared mutable state
The Ractor document included in Ruby 3.0.0 describes multiple ractors within one interpreter process. Each ractor has at least one native thread and can contain more than one Ruby thread. Threads inside a ractor share its global VM lock. Threads in different ractors can execute in parallel. This changes the scope of interpreter serialization without allowing all those threads unrestricted access to the same objects.
Objects fall into shareable and unshareable categories. Integers and certain other immutable values are shareable. A frozen object can be shareable when the objects it references are also shareable. Freezing only the outer container is therefore insufficient if its contents still include unshareable objects. Classes, modules, and ractor references have special shareable status, but that does not make all state reachable through them freely accessible.
A small worker can receive a value and return a result through its outgoing communication path. This example passes an integer into the worker without sharing a mutable result container.
worker = Ractor.new do
value = Ractor.receive
value * value
end
worker.send(12)
p worker.take
# Prints 144
send places the value in the worker's incoming queue. Ractor.receive waits if that queue is empty. Once the worker finishes, its block return value becomes available through take. The code does not capture a surrounding mutable object to communicate the answer. The integer message is shareable, so it does not need the deep-copy behavior used for ordinary unshareable messages.
Ractor supports both copying and moving unshareable objects. Sending such an object normally deep-copies it, where copying is supported. Passing move: true transfers access instead. The sender can no longer use the moved object and receives Ractor::MovedError if it attempts to do so. This is a runtime ownership rule with consequences for aliases held by the sending code.
Copying and moving impose different costs on an API. Copying preserves the sender's ability to use its object but duplicates the transferred structure. Moving avoids retaining shared access to that structure, which means the caller must stop using it. Some objects support neither operation. The document demonstrates rejection of thread objects, so a ractor message cannot be treated as an unrestricted container for arbitrary runtime state.
Communication still needs capacity and failure rules
The incoming queue is unbounded in the documented model, and send does not wait for a consumer to receive its message. That makes sending convenient, but it does not provide backpressure. A producer that continually outpaces a worker can accumulate queued objects. Limiting outstanding work remains a responsibility of the program's communication protocol.
The outgoing path behaves differently. Ractor.yield waits for a corresponding take, and take waits for an outgoing value. Ractor.select can wait across multiple ractors. Programs can consequently build worker pools and pipelines, but the selected communication method affects where execution waits. An incoming queue and an outgoing rendezvous have different capacity behavior even when both carry the same Ruby values.
Ports also have a lifecycle. Closing an incoming port prevents further sends, while messages already queued can still be received before an empty closed queue raises. Closing an outgoing port affects blocked receivers. A terminated ractor closes its ports, and remote exceptions can reach another ractor as Ractor::RemoteError. The examples in the document show that retrying a send to a terminated worker fails because its incoming port has closed. Recovering requires replacing the worker and deciding what happens to its work.
Isolation affects library design beyond explicit message passing. Non-main ractors cannot freely access global variables or class variables. A constant referring to an unshareable object is also restricted. A class being shareable does not make its class instance variables usable as an ordinary shared cache. Libraries built around mutable process-wide registries therefore have architectural work to do before their methods are suitable for multiple ractors.
I regard the explicit ownership rules as more useful than an expectation that familiar Ruby objects will become safe merely because execution is parallel. They also make adoption substantial. The release marks Ractor experimental, and the first creation emits an experimental warning. Existing object graphs and library state must fit the isolation model before the parallel execution facility is relevant to them.
Fiber scheduling coordinates waits
The fiber documentation at the release tag describes a separate form of concurrency. Fibers cooperate by transferring execution. A scheduler intercepts supported blocking operations so another fiber can proceed while one waits. This can overlap operations involving I/O, but it does not give two CPU-bound Ruby fibers simultaneous execution on separate cores.
A scheduler is installed for the current thread with Fiber.set_scheduler. Ruby defines the hooks, while a library supplies the event loop and scheduling policy. io_wait reports readiness for specified I/O events. Other hooks cover sleeping and blocking or unblocking fibers. The scheduler interface also includes an optional process-wait hook. An application adopting the interface depends on an implementation of those operations.
The hooks apply in non-blocking execution contexts. A fiber created with blocking: true bypasses them. Fiber.schedule delegates fiber creation policy to the installed scheduler. These distinctions let scheduler implementations control execution while ordinary library methods continue to look synchronous to their callers.
The Ruby 3.0 NEWS file identifies operations that may invoke these hooks, including I/O reads and writes, sleeps, queue operations, and joining threads. Support has boundaries. The announcement explicitly excludes IO.select, and the fiber document notes operating-system differences for non-blocking I/O. Installing a scheduler cannot make every possible blocking operation cooperate.
A method call that waits on I/O can now become a point where another fiber executes. The fiber document calls out this additional nondeterminism. Code that assumed an uninterrupted sequence between explicit fiber yields needs to account for scheduler-aware operations inside library calls. Cooperative execution still requires reasoning about shared state across those suspension points.
RBS keeps signatures separate from execution
RBS describes the structure and types of Ruby programs without inserting declarations into Ruby method bodies. The RBS 1.0 documentation covers methods, instance variables, inheritance, and mixins. Its library builds an environment of declarations and definitions that tools can inspect. A shared signature language allows tools to agree on the description of a library's API.
The signature syntax supports generic containers, unions, and optional values. A signature can distinguish a method accepting a block from the same method returning an enumerator when no block is given. That matters for Ruby because block-dependent APIs are common in its collection operations. A single fixed return type would describe them poorly.
For a simple application class, a separate signature could describe a nullable lookup result. Its argument and return declarations belong in the RBS file, while the method body remains Ruby code.
class Directory
def lookup: (String) -> String?
end
String? means a string or nil. The signature expresses a contract for callers and analysis tools. It does not implement the method, and storing this declaration beside Ruby source does not make the interpreter reject every value inconsistent with it. The declaration and implementation must continue to describe the same API.
Interface types describe supported operations without requiring one concrete class. RBS also provides untyped, which relaxes checking for a value. These choices allow signatures to represent Ruby's dynamic conventions, but a broad use of untyped necessarily supplies less information to a checker. The syntax can document uncertainty explicitly instead of pretending every boundary already has a precise contract.
Ruby 3.0 also bundles the experimental TypeProf analysis tool, which can produce prototype RBS signatures from Ruby code. Its language coverage and error detection are limited at this release. Generated signatures provide material to inspect, while the signature language supplies a common representation for further tooling.
The performance milestone has a defined workload
The announcement reports more than three times Ruby 2.0's performance on Optcarrot, an NES-emulation benchmark measuring single-thread execution. That result supports the project's stated milestone under the measured workload. It does not describe every program upgrading from Ruby 2.7, and it is independent of whether an application adopts Ractor or fiber scheduling.
MJIT work favors programs spending much of their time in a small set of repeatedly called methods. The announcement specifically says its optimization is not ready for Rails workloads spread across many methods. The release-tag NEWS details efforts to reduce generated code and optimize repeated operations. Reducing generated code matters because compiling more instructions can create its own pressure on instruction caches.
I find the concrete benchmark result useful when its denominator and workload remain attached. For an existing application, Ruby 3.0 also changes compatibility obligations. NEWS records the completed separation of positional and keyword arguments, so calls that relied on Ruby 2.7's deprecated conversions can now fail or behave differently. The interpreter upgrade delivers that language change immediately, while Ractor isolation, scheduler participation, and RBS contracts depend on deliberate changes to the application and its libraries.