Learn: General
November 14, 2025

Ruby 4.0: Your 2025 X-mas Gift from Matz

Ruby 4.0
Brad Herman
Bradley Herman

Upcoming Ruby 4.0 Features: What's Planned (And What Isn't)

It's nearly the holidays which means it's time for another Christmas Ruby release! You might have seen blog posts promising revolutionary changes and breakthrough performance, but here's what's actually happening: Ruby 4.0 is scheduled for December 25, 2025, and it's more evolution than revolution. The core team clarified that their version numbers don't follow strict semver, so 4.0 doesn't necessarily imply breaking changes. The focus is on refinements like Ractor communication improvements, core library enhancements, and performance gains through YJIT optimization.

Let me walk you through what's really coming, what you should prepare for, and what's just wishful thinking.

Ruby 4.0: Behind the Version Number

The bug tracker shows Ruby 4.0 has a December 25, 2025 release date with 93% completion. Matz himself clarified that "Ruby version numbers are not strictly semver. 4.0 does not necessarily imply breaking changes."

Translation: This isn't Python's 2-to-3 nightmare.

Ruby 4.0 enhances rather than overhauls. The core team has prioritized compatibility and stability over flashy features. The documentation outlines planned features in detail. While it includes breaking changes like frozen string literal enforcement and Ractor API redesign, these have been communicated through deprecation warnings since Ruby 3.4. If you're running Ruby 3.x in production, enabling deprecation warnings now and adding frozen string literal comments to your codebase will prepare you for a smoother transition.

But that doesn't mean there's nothing to get excited about...

Performance: The Numbers That Matter

Here's where Ruby 4.0 gets interesting. Benchmarks show very solid performance improvements:

Ruby 4.0 delivers measurable performance gains across real-world workloads:

  • 92.4% faster than Ruby 3.5.0 without JIT
  • 6.0% faster than YJIT in Ruby 3.3.6
  • 110.3% faster for Rails applications specifically

These aren't marketing numbers. They're measured using actual Rails applications, image processing, and CPU-intensive tasks.

Memory efficiency follows a predictable pattern. The YJIT compiler adds roughly one extra byte of memory per byte of Ruby bytecode. That 1:1 ratio gives you concrete planning numbers when you're sizing production systems.

The Missing Piece: ZJIT, the planned next-generation compiler, isn't ready for Ruby 4.0. Documentation states ZJIT performance data is "not yet ready for reliable benchmarks." The core team decided to ship Ruby 4.0 without ZJIT in production-ready status, prioritizing stability over experimental performance features.

For CPU-bound applications, Ruby 4.0 delivers meaningful speedups through Ractor-based parallelism, achieving true parallel execution on multiple CPU cores. For I/O-heavy Rails applications, improvements are more modest but consistent, with the Global VM Lock (GVL) automatically releasing during system calls to enable near-linear speedup through threading.

Breaking Changes: What You Need to Fix Now

Ruby 4.0 includes several planned breaking changes. The good news? You can start preparing today.

Frozen String Literals Become Mandatory

This is the big one. String literal mutation without explicit frozen_string_literal comments will raise runtime errors in Ruby 4.0.

# This will break in Ruby 4.0 str = "hello" str << " world" # FrozenError

Important Note for Ruby 4.0: In Ruby 4.0, string literal mutation without the frozen_string_literal magic comment will raise a runtime error. The code above works in Ruby 3.x but will fail in Ruby 4.0. Use this pattern instead:

# frozen_string_literal: true str = "hello".dup str << " world" # Works in both Ruby 3.x and Ruby 4.0

Action item: Add # frozen_string_literal: true to all your Ruby files now. Run with -W:deprecated to catch issues early. Preview2 confirms string literal mutation without explicit frozen_string_literal magic comments will be removed in Ruby 4.0, raising a runtime error instead of just producing a warning.

Ractor API Redesign

If you're using Ractors (Ruby's actor-based concurrency), the API is changing significantly. Ractor.yield and Ractor#take are being removed in favor of a new port-based communication system using the Ractor::Port class. Additionally, Ractor#join and Ractor#value provide synchronization capabilities similar to the Thread API.

# New way ractor = Ractor.new do port = Ractor.current.default_port port.send(42) end result = ractor.default_port.receive

This affects anyone doing CPU-bound parallel processing with Ractors. Most Rails applications won't notice, since Rails applications typically handle IO-bound workloads where the Global VM Lock (GVL) is automatically released during system calls, enabling concurrent execution even within a single Ractor.

What is a Ractor anyway? (Parallelism in Ruby)

Ruby's Ractor (short for "Ruby Actor") solves a fundamental limitation in Ruby's concurrency model. Let me break it down for you in plain terms.

Traditional Ruby threads share memory, which means they can't truly run in parallel due to the Global VM Lock (GVL). That lock prevents multiple threads from executing Ruby code simultaneously, even on multi-core systems.

Ractors change this by creating isolated memory spaces that can run truly parallel on multiple CPU cores.

Ractors vs. Threads: The Key Difference

Think about the difference this way:

  • Threads: Share memory, must take turns executing (concurrent but not parallel)
  • Ractors: Isolated memory, can run simultaneously (truly parallel execution)

This isolation is Ractors' superpower. Since each Ractor has its own isolated memory, they can execute Ruby code simultaneously without memory conflicts.

When Should You Use Ractors?

Ractors shine in CPU-bound tasks where you need to:

  • Process large datasets independently
  • Perform intense calculations across multiple cores
  • Execute computationally expensive operations in parallel

You might not need Ractors if your application is primarily:

  • I/O-bound (database queries, API calls, file operations - ie: a Rails app)
  • Using primarily third-party C extensions that aren't Ractor-safe
  • Running single-threaded workloads

For I/O-bound work, regular threads still work great because the GVL releases during system calls anyway.

Simple Ractor Example

Here's a basic example of how to use Ractors in your code:

# Create two Ractors to perform CPU-intensive work in parallel r1 = Ractor.new("r1") do |name| # Expensive computation isolated in this Ractor result = (1..1_000_000).map { |i| i * i }.sum "#{name} computed sum: #{result}" end r2 = Ractor.new("r2") do |name| # Different computation running truly parallel result = (1..1_000_000).map { |i| i * i * i }.sum "#{name} computed sum: #{result}" end # Get results from both Ractors puts r1.take puts r2.take

In Ruby 4.0, with the updated Ractor API, this would become:

r1 = Ractor.new("r1") do |name| result = (1..1_000_000).map { |i| i * i }.sum Ractor.current.default_port.send("#{name} computed sum: #{result}") end r2 = Ractor.new("r2") do |name| result = (1..1_000_000).map { |i| i * i * i }.sum Ractor.current.default_port.send("#{name} computed sum: #{result}") end puts r1.default_port.receive puts r2.default_port.receive

Sharing Data Between Ractors

Ractors communicate by passing messages, not sharing memory. Only certain objects can be shared between Ractors:

  • Immutable objects (frozen strings, frozen arrays, etc.)
  • Special shareable objects (Ractor itself)
  • Copied objects (gets duplicated when sent between Ractors)

This explicit communication model makes parallel programming safer by preventing the race conditions and deadlocks common in shared-memory concurrency.

Real-World Use Case

Here's where Ractors make sense in production:

def process_images(image_paths) # Create one Ractor per CPU core workers = Ractor.new(Runtime.cpu_count) do image_paths.each_slice(image_paths.size / Runtime.cpu_count).to_a end # Each worker processes its chunk of images in parallel results = workers.map do |chunk| Ractor.new(chunk) do |paths| paths.map do |path| image = Image.load(path) image.apply_filters image.optimize image.save end end end # Collect results results.map(&:take).flatten end

Ractors aren't a magic solution for all performance issues, but they provide true parallelism when you need it, especially for CPU-intensive operations on multi-core systems.

C Extension API Changes

If you maintain C extensions, several functions were removed in Ruby 3.4 and won't be coming back: rb_newobj, rb_newobj_of, and related macros. No compatibility shims, no gradual deprecation. They're just gone.

Most developers won't hit this, but gem maintainers and C extension developers need to audit their C code for removed C API functions like rb_newobj and rb_newobj_of that were eliminated in Ruby 3.4.

Syntax Enhancements: Small Improvements, Big Impact

Ruby 4.0's syntax changes are subtle but significant, including logical binary operators now able to continue lines from the previous line, and enhanced multiline operator support for more fluent conditional expressions.

Multiline Logical Operators: You can now start lines with &&, ||, and, or or for cleaner multiline conditions:

# Ruby 3.5 allows this if user.active? && user.verified? && user.subscription.current? grant_access end # while Ruby 4.0 will allow this if user.active? && user.verified? && user.subscription.current? grant_access end # or if user.active? and user.verified? and user.subscription.current? grant_access end

Improved Kernel#inspect: Custom objects can now control what instance variables appear in inspect output by defining a private instance_variables_to_inspect method.  This is super helpful for hiding sensitive information from logs for instance:

class DatabaseConfig def initialize(host, user, password) @host, @user, @password = host, user, password end private def instance_variables_to_inspect [:@host, :@user] # Hide @password from inspect output end end conf.inspect #=> "#<DatabaseConfig:0x... @host=\"localhost\", @user=\"root\">"

Small changes. But they make code cleaner without forcing you to rewrite anything.

Core Library: Less Boilerplate, More Built-ins

Two frequently-used gems are joining Ruby's core: Set and Pathname.

# Ruby 4.0: Set and Pathname promoted to core classes my_set = Set.new([1, 2, 3]) path = Pathname.new('/usr/local/bin')

This eliminates dependency management for basic data structures and file operations by promoting Set and Pathname from default stdlib/gems to core Ruby classes.

New Precision Math Functions:Math.log1p and Math.expm1 (Feature #21527) handle high-precision computations for small values without catastrophic cancellation. These specialized functions use Taylor series approximations to maintain accuracy when |x| ≪ 1, making them particularly useful for financial calculations and scientific computing where precision matters.

Socket Timeouts: Native timeout support in socket connections via the open_timeout keyword argument added to Socket.tcp and TCPSocket.new (Feature #21347).

# Ruby 4.0 socket = Socket.tcp('example.com', 80, open_timeout: 5)

Socket connection timeouts are now natively supported without threading workarounds.

Concurrency: Evolution, Not Revolution

Ruby 4.0 maintains the Global VM Lock. This disappointed some developers who wanted true multithreading, but the decision makes sense.

Removing the GVL would require extensive architectural changes to Ruby's core. Instead, Ruby 4.0 achieves true parallelism through multiple isolated Ractor instances and continues to improve the concurrency tools you already have:

  • Per-object locks or atomic reference-counting would be required
  • Widespread C extension refactoring that would break backward compatibility
  • Performance degradation for single-threaded workloads would follow

For I/O-bound applications: Threading still works great because the GVL releases during system calls. You get near-linear speedup for database queries, API calls, and file operations.

For CPU-bound applications: Ractors provide true parallelism across CPU cores. The API redesign removes the experimental Ractor.yield and Ractor#take methods in favor of explicit Ractor::Port-based communication and Ractor#send/receive primitives, making inter-Ractor communication more explicit and reliable.

Ractor Threading Improvements: Ruby 4.0 builds on experimental work with Ractor-compatible threading that began in Ruby 3.3. While true M:N threading (mapping many Ruby threads to fewer OS threads) was explored through the MaNy project, it's currently implemented only within individual Ractors and remains disabled by default. This architecture could potentially improve performance for applications handling thousands of concurrent connections by reducing per-thread overhead, but full implementation across Ruby's threading model is likely not coming in the 4.0 release.

Developer Experience: Better Tools, Clearer Errors

The debug gem achieves 20× performance improvement over legacy byebug per analysis. Instead of byebug, you'll use binding.break for cleaner debugging:

def complex_method(data) processed = transform(data) binding.break # New debug API in Ruby 4.0, replaces byebug finalize(processed) end

IDE integration continues improving. VS Code's Ruby LSP provides context-aware completion, go-to-definition, and inline documentation. RubyMine adds configurable memory management options to optimize performance and prevent resource exhaustion during large refactoring sessions.

RubyGems and Bundler see 20-50% faster dependency resolution when version lengths differ. This performance improvement comes from optimizations in Gem::Version#<=> comparisons. You'll notice it during bundle install.

Migration Strategy: Start Now, Finish Smoothly

Upgrade guides and case studies recommend a three-phase migration plan:

Phase 1 (Do This Week):

  • Enable deprecation warnings immediately. Run your application with ruby -W:deprecated or set Warning[:deprecated] = true in your test environment
  • Fix warnings as they appear. This is critical because frozen string literal mutation without explicit frozen_string_literal magic comments will raise runtime errors in Ruby 4.0
  • Add frozen string literal comments to new files. Use a linter to enforce this going forward

Phase 2 (Before Ruby 4.0 Release):

  • Set up CI to test against Ruby 4.0 preview releases when available
  • Use the "dual boot" approach—keep your application compatible with both Ruby 3.x and 4.0 throughout the migration
  • Run parallel CI jobs testing both versions, as documented in GitLab's guide
  • Check gem compatibility using RailsBump to audit gems for Ruby 4.0 compatibility

Phase 3 (Post-Release):

  • Deploy to staging first
  • Monitor for warnings in production using error tracking tools and Ruby's Warning module hooks
  • While Ruby 4.0 aims for backward compatibility, staging validation remains essential for catching edge cases

The beauty of Ruby 4.0's migration approach: you can migrate incrementally through a three-phase process without requiring big-bang deployments.

What Didn't Make the Cut

Several proposed features got postponed beyond Ruby 4.0:

  • Ruby::Box (experimental module sandboxing)
  • Refinements and nested methods enhancements
  • Namespace on Read
  • VM-level enhancements
  • Object#deep_freeze
  • Default frozen strings
  • Comprehensive gradual typing

Ruby::Box (module sandboxing) remains experimental. When asked about Ruby::Box inclusion in Ruby 4.0, Matz responded directly: "Ruby Box is an experimental feature. We have no concrete plan to include it in Ruby 4.0. It remains a research prototype, not yet production-grade."

Namespace on Read for isolated library loading was initially tagged for 4.0 but got deferred due to incomplete backward-compatibility validation and remains in experimental/discussion status rather than being confirmed as part of Ruby 4.0.

Default frozen strings and gradual typing remain under review without firm timelines, as clarified by core team members in official discussions.

The pattern is clear: Ruby's core team is prioritizing stability and thorough validation over rushing experimental features into releases. While this ensures production-grade code quality, it means some proposed features remain in extended development cycles. The Ruby core team explained that features like Ruby::Box and namespace-on-read improvements require real-world experimentation before major release consideration, reflecting a deliberate approach to language evolution.

The Bigger Picture

Ruby 4.0 represents Ruby's maturation, not its revolution.  While the language continues to evolve thoughtfully, the team is focused on making existing patterns better rather than chasing flashy features or unnecessary breaking changes.

Performance improvements are real and measurable. Ruby 4.0 benchmarks show 92.4% speed gains over the interpreter baseline and 6.0% improvement over YJIT 3.3.6. Breaking changes are minimal and well-telegraphed, with deprecation warnings introduced in Ruby 3.4 giving developers time to prepare. New features solve actual problems without creating new ones, from Ractor::Port communication to enhanced I/O timeout support.

For teams running Ruby in production, this addresses key concerns:

  • The frozen string literal migration is well-supported with deprecation warnings since Ruby 3.4
  • The three-phase migration path has proven successful in production migrations
  • The breaking changes are manageable when addressed systematically using tools like deprecation_toolkit and the ratchet testing strategy

Start preparing now with deprecation warnings and frozen string literals. By December 25, 2025, you'll be ready for Ruby 4.0: an upgrade that improves your application without breaking it.

The future of Ruby isn't about becoming a different language. It's about becoming a better version of itself.