Upgrading a Rails app from Ruby 3.4.4 to 4.0.3: what actually breaks
Ruby 4.0 is the first major bump in over a decade, but a Rails 8.x app usually upgrades in a day of focused work. Most of the pain is not in the language: it is gems that left the default set (cgi, ostruct, logger, benchmark), the Ractor::Port rewrite, Kernel#open("| cmd") no longer spawning a process, and a handful of changes that break tests rather than code (Set#inspect output, Net::HTTP no longer defaulting Content-Type on bodies, new backtrace frames that confuse Sentry/Datadog grouping rules).
The post walks through a three-phase plan: prepare on 3.4.4 with RUBYOPT="-W:deprecated" and a Gemfile audit, switch locally and in CI with a clean bundle pristine, then stage and roll out with Kamal and a 48-hour soak. It also covers why 4.0.3 is the minimum worth targeting (the ERB fix for CVE-2026-41316, which hits any Rails app with a Marshal-based cache store), what you get for free from the new allocator and GC, and why YJIT stays on and ZJIT stays off until 4.1.
Upgrading from Ruby 3.4.4 to Ruby 4.0.3: a step-by-step guide for Rails apps
Post a comment