Ruby 3.3 preview 3 exposes the thread scheduler's compatibility boundary
The third preview adds M:N scheduling with explicit native-extension limits and distributes Prism as a parser library, alongside runtime control over YJIT startup.
A Ruby thread and an operating-system thread do not have to be permanently paired. Separating them can reduce the cost of creating and managing many Ruby threads, but it changes assumptions made by native libraries. Ruby 3.3 preview 3 exposes that tradeoff through an M:N scheduler that remains disabled by default on the main Ractor.
The November 12 preview announcement describes the scheduler alongside Prism, runtime YJIT enablement, and other implementation work. The thread change is particularly informative because the release states the compatibility boundary instead of treating a lower thread count as an unconditional improvement.
I find Ruby's consistent object model approachable, but native integration creates responsibilities outside that model. Thread-local state held by a C library is one example where implementation details become part of an application's behavior.
Ruby threads share native execution resources
The M:N scheduler proposal, by Koichi Sasada, explains the historical alternatives. Earlier Ruby used multiple Ruby threads on one native thread. Later implementations paired Ruby threads with native threads and used the global VM lock to control Ruby execution.
A one-to-one model makes some blocking operations easier to support because a blocked native thread can remain associated with the operation. It also incurs the cost of many native threads when an application creates many Ruby threads or Ractors.
The M:N model instead schedules multiple Ruby threads over a smaller collection of native threads. The preview announcement says this reduces thread creation and management costs. Its default maximum native-thread setting is eight, configured through RUBY_MAX_CPU.
That number is not an unconditional cap on all operating-system threads a process can ever create. The announcement says additional native threads may be used for blocking operations. The proposal distinguishes shared execution threads from threads dedicated to blocking work.
More Ruby threads do not remove the Ractor execution limit
The preview retains one executing Ruby thread per Ractor at a time. For a single-Ractor application, the announcement therefore describes one native thread being used under the enabled M:N model, apart from the blocking-operation considerations.
Multiple Ractors provide the separate units that can execute in parallel. Replacing one-to-one scheduling does not itself permit all Ruby threads within one Ractor to execute Ruby code simultaneously.
This distinction is necessary when interpreting the change as a performance feature. Reduced scheduling overhead and increased parallel application execution are different outcomes. A scheduler can improve the former without changing the language runtime's rules for the latter.
The default also matters. Main-Ractor threads keep the existing behavior unless M:N scheduling is explicitly enabled. Non-main Ractors use the new scheduling model according to the preview announcement.
Native thread-local state creates the compatibility problem
The proposal gives a concrete failure scenario. A Ruby thread calls a native library that records data in operating-system thread-local storage. If the Ruby thread later runs on another native thread, another call into that library may not find the state stored on the original thread.
The Ruby thread's logical identity has not changed, but the native library observes a different execution thread. That difference can break an extension whose assumptions depend on the original association.
Keeping main-Ractor scheduling unchanged by default limits exposure for existing applications. The preview provides RUBY_MN_THREADS=1 for explicitly enabling the new model there. The setting is a compatibility choice, not merely a request for more parallelism.
This is the design detail I consider most useful for extension maintainers. The relevant question is whether a library assumes stable native-thread identity across calls. A test that only checks the number of Ruby threads would not examine that assumption.
Prism supplies a separate parser interface
The preview also introduces Prism as a default gem. It describes a portable, error-tolerant recursive-descent parser, available as a C library and as a Ruby interface for tools that need to parse Ruby source.
Prism.parse returns a ParseResult containing the syntax tree. Prism.dump serializes the tree, and Prism.parse_comments retrieves comments. These are different products a tool might need from the same source text.
Distributing Prism does not mean that every Ruby execution path has already switched to it. The announcement presents its internal CRuby use as future work and says tooling can use it in place of Ripper. Lrama remains the parser generator replacing Bison for the existing grammar workflow.
YJIT can begin after application startup
Preview 3 adds RubyVM::YJIT.enable. The announcement says this allows enabling YJIT at runtime without changing command-line arguments or environment variables. It can also delay compilation until application boot has finished.
The preview uses --yjit-disable when other YJIT options should apply while compilation remains disabled at boot. Those names are the November interface, distinct from the paused-start terminology used in earlier previews.
Together, these changes make several implementation boundaries explicit. Prism gives tools a parser API, YJIT gains a runtime startup decision, and M:N scheduling separates Ruby-thread identity from native execution resources. The scheduler's opt-in main-Ractor behavior preserves a concrete compatibility boundary while native extensions adapt to that separation.