Ruby 4.1 preview gives Ractors more independent heaps
Ruby 4.1's first preview moves ordinary garbage collection into individual Ractors and sharpens ownership rules. Windows YJIT and autoload_relative address separate limits in the development environment.
Parallel execution loses some of its value if one worker's allocation forces unrelated workers to stop. Ruby 4.1.0-preview1, released on October 12, 2026, moves the default garbage collector toward Ractor-local work. Each Ractor collects its own heap on its own thread, with global collection retained for cases that need it. The change connects parallel execution to the ownership of the objects being collected.
This is a preview, with Ractors still described as approaching the end of experimental status. Its changes provide something concrete to evaluate, without making the preview a promise that existing Ractor applications can upgrade unchanged. The release documents visible differences in result retrieval and monitoring, as well as restrictions on modifying classes created elsewhere.
I find Ruby more consistent and approachable than the Perl object conventions I came from. My long familiarity with it does not make it my automatic choice for a new system. Ractor work is interesting because it addresses a specific reason to reconsider a runtime's capabilities, with an account of both execution and memory ownership. That is more informative than a generic claim that a language has become faster.
Ordinary collection becomes local
The preview announcement says stop-the-world collection remains for explicit full collection, growth of shareable objects and reclaiming dead Ractors' heaps. Routine collection can otherwise proceed independently. This does not eliminate every global pause. It reduces the work that necessarily involves all Ractors and identifies circumstances where coordination still occurs.
GC.start defaults to global collection when the relevant multiple-Ractor state exists. Passing global: false requests local collection, while global: true makes the global choice explicit. With one Ractor running, the keyword has no effect. These details prevent a local-collection example from being mistaken for a universal change to the meaning of every existing explicit GC call.
The statistics API also gains scope. GC.stat accepts a Ractor scope and a global scope, with local as a synonym for Ractor. Global reporting includes cumulative collection counts and marking and sweeping CPU time across Ractors, including history from destroyed object spaces. The announcement says obtaining those totals does not require a stop-the-world snapshot.
A new global-collection count distinguishes this work from local major collections. Each global collection is attributed to the Ractor that initiated it, allowing the counts to add up across Ractors. That is important for interpreting measurements. A count whose meaning changes between local and global activity can otherwise produce misleading comparisons even when the collector itself works correctly.
The default collector also changes from a freelist allocator to a bump-pointer allocator. The release lists that as a separate implementation improvement. It should not be treated as evidence that every application has the same speedup, or that allocation-heavy parallel workloads represent all Ruby programs. The relevant mechanisms include allocation strategy and collection scope, which can affect different workloads differently.
Class ownership supplies a mutation boundary
The Ractor documentation shipped with this preview explains the parallel model. Threads within one Ractor share its GVL, while different Ractors can execute Ruby in parallel. Most objects are unshareable and belong to the Ractor that instantiated them. Communication can copy or move such objects, and genuinely shareable objects follow different rules.
The preview sharpens class and module ownership. A class or module records the Ractor that created it, and only that owner may modify it. Defining methods, changing visibility and registering an autoload are among the operations subject to that rule. Libraries loaded by the main Ractor have classes owned by the main Ractor, so a worker cannot freely monkey-patch them.
In exchange, a Ractor can make fuller use of classes it creates itself, including unshareable values held in their constants or variables. That gives a worker local behavior and local state without granting every worker write access to the same global definitions. The tradeoff is visible to code that previously expected to modify a shared class from a non-main Ractor.
The documentation qualifies what ownership guarantees. Reads remain available, and class modification is not atomic. A non-owner can observe the owner partway through modifications. Including a module also accepts changes to the ancestor structure performed through that module's owner. Class ownership therefore establishes write authority without promising every reader a transactional snapshot.
That qualification matters for the design of library initialization. Restricting who may write is useful, but it does not make arbitrary runtime reconfiguration invisible to other Ractors. Initialization and later mutation still need a deliberate relationship to concurrent readers. The documentation names that limit directly instead of presenting ownership as a substitute for every synchronization decision.
Communication and supervision change too
Ractor#value now returns its result only once. A second call raises Ractor::Error. Code treating the value as a repeatedly readable result needs to account for that difference. The operation participates in transferring an execution result, with stronger consequences than a simple accessor over an immutable field.
Monitoring messages also become more informative. A monitor receives an array identifying both the Ractor and whether it exited or aborted. One port can therefore watch several workers without losing the identity of the one that finished. The announcement says the array is built for the receiving Ractor so repeated monitoring does not leave shareable objects behind.
Garbage-collector enablement also receives a coordination rule. One Ractor's GC.enable no longer overrides another Ractor's GC.disable. The calls act as local holds on a process-wide switch. This is another case where giving a worker independent control still needs a defined relationship to global behavior.
Native extensions have corresponding responsibilities. The preview says GC address registration becomes associated with the calling Ractor. A registered address must not point to another Ractor's unshareable object. The collector that owns the registration would not mark that foreign value correctly, potentially leaving a reference to freed memory. Extension compatibility therefore involves ownership rules as well as compiling against a new interpreter.
Windows YJIT has a specific supported configuration
The preview adds YJIT on Windows for x64 MSVC builds. Its configuration requires Rust with the x86_64-pc-windows-msvc target, and only release builds are supported. MinGW is not included. Saying merely that YJIT now works on Windows would omit constraints that decide whether a particular installation can use it.
This is separate from ZJIT, the newer compiler introduced experimentally in Ruby 4.0. The preview's Windows support announcement names YJIT. The existence of multiple JIT implementations makes the exact name consequential when discussing build requirements or expected behavior. A performance comparison has to identify the compiler actually enabled.
The release also raises the minimum MSVC and Windows requirements for MSVC-built Ruby. These platform changes belong alongside the JIT announcement in a deployment assessment. A new compiler capability does not establish compatibility with every environment that ran an older interpreter. The supported build and operating-system combination is part of the feature's practical scope.
Startup behavior changes independently of JIT support. The preview loads error_highlight, did_you_mean and syntax_suggest lazily when displaying an error. Process.warmup can load them eagerly for pre-forking servers so their code remains in copy-on-write shared memory. That gives applications an explicit choice about when to incur this initialization work.
Relative autoloading follows a long argument
Kernel#autoload_relative and Module#autoload_relative resolve paths relative to the calling file, analogous to require_relative. They let code register a constant's file without constructing an absolute path or changing $LOAD_PATH. This is a small API addition with a substantial history behind it.
Feature #15330 records Marc-Andre Lafortune's proposal and the debate over making autoload easier to use. Matz initially objected because of autoload's nondeterministic behavior and implementation problems. Other participants argued that existing usage and framework requirements made removing it unrealistic. The discussion eventually records acceptance of the relative form.
The distinction is between whether delayed loading should exist and how a file path should be expressed when it does. Relative resolution addresses the latter. It does not remove the timing behavior that motivated the earlier objections. That makes the accepted API easier to understand without attributing a complete redesign of autoload to a new method name.
My appreciation for Ruby's readable interfaces fits this addition, while the Ractor changes interest me for a different reason. They describe boundaries the runtime needs to enforce as execution becomes more parallel. For an application evaluating the preview, the substantive compatibility questions include who owns class definitions and how results move between workers. Those contracts determine whether local collection can operate within the application's existing concurrency design.