Skip to content
Allerin, go to homepage

Rails upgrades to 8.1, one version at a time

Rails 7.2.x stopped receiving security fixes on 9 August 2026, and 7.0 and 7.1 took their final releases, 7.0.10 and 7.1.6, on 29 October 2025. This page is for the engineering lead whose application is on 4.2 through 8.0 and needs to land on 8.1.3.1 or later without a freeze. It is not a monthly plan and it is not a flat quote; both start after the assessment.

The support clock, as of 4 September 2026

SeriesBug fixes untilSecurity fixes until
Rails 8.1.x10 October 202610 October 2027
Rails 8.0.xended 7 May 20267 November 2026
Rails 7.2.xendedended 9 August 2026
Rails 7.0 and 7.1final releases 7.0.10 and 7.1.6 on 29 October 2025ended

Ruby 4.0 and 3.4 are in normal maintenance; 3.3 is security-only until an expected 31 March 2027; 3.2 reached end of life on 1 April 2026. Rails 8.0 and 8.1 need Ruby 3.2.0 or newer, so a Rails version alone does not tell you whether you are supported.

CVE-2026-66066 (CVSS 9.5, advisory dated 29 July 2026): arbitrary file read and possible remote code execution affecting Active Storage applications that use libvips and accept untrusted image uploads. Generating variants is not an additional requirement. The advisory lists fixes in 7.2.3.2, 8.0.5.1 and 8.1.3.1; the patched code requires libvips 8.13 and ruby-vips 2.2.1 or newer. Earlier Rails series have no patched release listed; the advisory documents workarounds and secret-change requirements.

How we upgrade

One minor version at a time, which is what the upgrading guide says and what every serious firm does. bin/rails app:update, then the generated new_framework_defaults_X_Y.rb one setting at a time, then config.load_defaults. Ruby bumps between Rails hops, not alongside them. Dual boot with both lockfiles in your CI, so each hop is its own branch, pull request and deploy. Deprecations cleared on the current version before the next one is attempted. A gem verdict for every dependency, because gems block more hops than the framework does. The front end (Webpacker, Turbolinks, rails-ujs) scoped as its own workstream. Cache format and cookie changes deployed in two steps where the guide says two steps.

None of this is novel; the tooling is open source. What we add is the judgment on which gem to replace rather than patch, which default to flip in which deploy, and what the per-app log of your own breakage says about the next hop.

The assessment

For an upgrade, the review produces the gem verdict list (upgrade, fork, vendor, replace, delete), the deprecation inventory, the load_defaults gap setting by setting, the Ruby and image path, the coverage map on the money, authentication and personal-data paths, the rollback plan, and the hop sequence with the front-end decision scoped on its own.

4-8 weeks for a single major-version jump, after a 1-2 week assessment. Multi-version paths (for example 5.2 to 8.1) run longer and are scoped and quoted in the assessment rather than estimated up front. Public market ranges are quoted on this page where they exist, labeled as such, and none of them is our estimate for your application.

The ladder: what breaks, what we do, what you get, per jump

Rails 4.2 to 5.0, 5.1, 5.2

What breaks
protected_attributes is unsupported on Rails 5; replace it with strong parameters. Rails 5.0 deprecates halting AR/AM callbacks by returning false; migrate to throw :abort before enabling the new behavior. Review belongs_to_required_by_default before adding required-association validations. ActionController::Parameters no longer inherits from HashWithIndifferentAccess. assigns and assert_template move to the rails-controller-testing gem. Every existing migration must be tagged ActiveRecord::Migration[4.2]. Ruby 2.2.2 is the floor. 5.1 removes render :text, render :nothing and redirect_to :back. 5.2 embeds expiry metadata in signed and encrypted cookies that Rails below 5.2 cannot read; opt out with config.action_dispatch.use_authenticated_cookie_encryption = false while both versions run.
What we do
Inventory every model that relies on attr_accessible and every callback that returns false before touching the Gemfile. Tag migrations in one commit. Move cookie encryption in a deploy of its own so nobody is logged out. Treat Ruby 2.2.2 to 2.5 as its own step, because Rails 6 needs 2.5.
What you get
An app on 5.2 with strong parameters throughout, callbacks that halt explicitly, cookies readable by the next hop, and a per-model log of what changed. This is a multi-version path and is scoped in the assessment.
Public market range
No published effort range without a price attached exists for this path (checked 4 September 2026). Not estimated here.

Rails 5.2 to 6.0 to 6.1

What breaks
Loading Rails 6.0 defaults selects Zeitwerk on CRuby; existing 6.0/6.1 applications can retain classic mode during migration. Check file/constant naming, inflections, explicit namespaces and reload lifecycle before Rails 7 removes classic. Review require_dependency in engines supporting both loaders; run bin/rails zeitwerk:check. 6.0 adds config.hosts, moves npm packages to the @rails scope, does not activate Webpacker on upgrade, and embeds purpose metadata in cookies that Rails below 6.0 cannot read (config.action_dispatch.use_cookies_with_metadata = false while both run). 6.1 removes update_attributes, makes where.not generate NAND instead of NOR, makes config_for symbol-keyed, requires the image_processing gem for Active Storage, and adds the service_name column migration. Ruby 2.5 is the declared floor for Rails 6.0/6.1. Plan Ruby changes against the exact Rails patch, native libraries and locked gems; the floor does not establish later-Ruby compatibility.
What we do
Run Zeitwerk as its own workstream on 6.x, not something found during 7.0. Grep every where.not with more than one condition; that break passes a shallow suite. Deploy the cookie metadata change separately. Use an isolated environment for unsupported historical runtimes and choose each Ruby step from a resolved, tested overlap between the two Rails bundles.
What you get
An app that passes zeitwerk:check, a where.not audit with each multi-condition call reviewed, cookies readable across the hop, and an explicitly tested Ruby/dependency combination for the 7.0 step.
Public market range
Rails Fever's hop-by-hop guide (22 April 2026) puts 6.0 to 6.1 at 1 to 3 days and gives no figure for 5.2 to 6.0. Those are the author's estimates, not project data, and not ours.

Rails 6.1 to 7.0

What breaks
The heaviest modern hop. Classic autoloading is deleted: config.autoloader= is gone, the private API of ActiveSupport::Dependencies is gone, and initializers that autoload reloadable constants outside to_prepare raise NameError. Spring below 3.0.0 fails with undefined method mechanism=. The rails gem no longer depends on sprockets-rails. Webpacker is retired. Hotwire/importmap are generated-app choices; existing Turbolinks/UJS and asset tooling need a compatibility and maintenance decision of their own. Key generator and ActiveSupport::Digest move from SHA1 to SHA256: register a rotator or existing cookies stop reading; ETags and cache keys invalidate. The 7.0 cache format cannot be read by 6.1, so deploy the old format first and flip in a later deploy. The Active Storage variant processor default becomes :vips, the setting CVE-2026-66066 later turned on. button_to emits PATCH for persisted records.
What we do
Scope the front end separately and first, because the Webpacker decision (jsbundling, importmap or Shakapacker) can cost more than the framework side. Ship the cookie rotator before the digest flip. Two-step the cache format. Decide :vips versus :mini_magick with the libvips version in your image in hand. Change Ruby separately using an exact-patch and locked-dependency overlap. Rails 7.0 declares Ruby 2.7 as its floor; deploying the unsupported Ruby 3.0 intermediate is not a requirement.
What you get
Zeitwerk-only boot, Spring 3 or no Spring, a front end on a supported pipeline with the reason written down, cookies that survived the digest change, a cache that was never unreadable, and a documented variant-processor decision.
Public market range
Rails Fever (22 April 2026) puts this hop at 3 to 7 days and calls it the largest jump; Ecliptic Ideas (23 July 2026) says roughly a week per major hop for a mid-sized app with decent tests and months for an app several majors behind with a Webpacker front end. Both are estimates, not project data, and not ours.

Rails 7.0 to 7.1

What breaks
The largest defaults file: 28 settings, including default_column_serializer (turns off default YAML column serialization), cache_format_version, message_serializer, sanitizer_vendor and dom_testing_default_html_version. The development/test secret file moves from tmp/development_secret.txt to tmp/local_secret.txt; preserve its contents to retain local sessions and signatures. Production and other environments are not affected by this file rename. Autoloaded directories leave $LOAD_PATH, so an explicit require of an app file raises LoadError. Rails.application.secrets starts warning. The 7.1 cache format needs a two-phase deploy. MemCacheStore and RedisCacheStore pool by default. show_exceptions changes from booleans to :all, :rescuable and :none. Rails.logger becomes a BroadcastLogger. Active Record Encryption moves to SHA-256; existing SHA-1 data needs configuration. ActionController::Parameters rejects non-string, non-symbol keys. The ActiveSupport::Deprecation singleton is deprecated for Rails.application.deprecators. With load_defaults 7.1, a return, break or throw inside a transaction block commits instead of rolling back (commit_transaction_on_non_local_return). Named gem casualties: PaperTrail 12.x (fixed by 15.x), data-migrate below 9.2.0, and 23 gems catalogued with undocumented 7.1 incompatibilities.
What we do
Walk all 28 settings with you and record the production consequence of each. Preserve the development/test secret when moving its file. Review explicit requires of app files. Exercise non-local exits from transaction blocks and capture the resulting warning and data outcome before choosing the new behavior. Two-phase the cache format. Translate show_exceptions true/false to :all/:none to preserve existing exception behavior, or adopt :rescuable deliberately and update assertions.
What you get
A load_defaults 7.1 app with a 28-line decision record, no invalidated sessions, a cache that stayed readable, a reviewed list of transaction sites, and the deprecation inventory that feeds 7.2.
Public market range
Rails Fever (22 April 2026): 1 to 2 days, an estimate. One published single-app account: about two hours on a 584-test, 82 percent covered app (Garcha, 26 January 2026). Neither is our estimate; the second is one app.

Rails 7.1 to 7.2

What breaks
Ruby 3.1 becomes the declared floor; inspect the selected Ruby distribution and locked gems for Psych and standard-library packaging changes. Rails.application.secrets is removed. cache_format_version = 6.1 support is removed; third-party stores that never learned the 7.x format fail at boot with Unrecognized ActiveSupport::Cache.format_version. alias_attribute bypasses custom methods and refuses non-attributes. The positional coder in serialize is removed. Active Job supports deferring enqueues until after transaction commit; job configuration and queue-adapter behavior determine whether a call defers. Tests respect the configured queue adapter. ActiveSupport::Deprecation singleton delegation is removed. Rails 7.2 still ships rails-ujs; existing UJS use is a separate compatibility decision. clear_active_connections! and friends are dropped. The 7.2 defaults add enqueue_after_transaction_commit, web_image_content_types (webp), validate_migration_timestamps, postgresql_adapter_decode_dates and yjit.
What we do
Read every perform_later inside a transaction, because that timing change has no deprecation to grep. Confirm the test suite's queue adapter and that the jobs you think run in tests still run. Evaluate YJIT on an eligible Ruby build with representative workload measurements.
What you get
A 7.2 app with a reviewed list of transaction-and-job sites, a recorded YJIT decision, and a boot that does not depend on the 6.1 cache format anywhere.
Public market range
Rails Fever (22 April 2026): 1 to 2 days, an estimate. Not ours.

Rails 7.2 to 8.0

What breaks
Ruby 3.2 becomes the floor. An enum name must be positional; migrate enum status: {...}, _prefix: true to enum :status, {...}, prefix: true. Keyword options remain supported. ConnectionPool#connection, ActiveSupport::ProxyObject, form_with model: nil and unregistered database adapters are removed. Loading the 8.0 defaults sets Regexp.timeout only when it is nil, using a one-second value, and selects to_time_preserves_timezone = :zone and strict_freshness = true. Existing defaults and explicit overrides must be reviewed separately from the gem version. Benchmark.ms, the Azure storage service and multi-path routes are deprecated. Propshaft and the Solid backends are defaults for generated applications; existing applications can retain compatible Sprockets, Sidekiq and Redis dependencies. Kamal, Thruster and the authentication generator are optional adoption decisions. The authentication generator is not run automatically, even for a new app.
What we do
Resolve removed APIs and incompatible gem requirements while preserving the existing queue, cache, assets and authentication. Review app:update conflicts and enable framework defaults with behavior checks. Keep backend changes separate, then continue to 8.1 as the supported destination.
What you get
An 8.0 app on its existing queue, cache and asset pipeline, with the to_time sites reviewed and a written list of the modernization options and their prerequisites.
Public market range
Rails Fever (22 April 2026): 1 to 3 days, an estimate; Ecliptic Ideas (23 July 2026): an afternoon for an app on 7.2 with a green suite. Neither is ours.

Rails 8.0 to 8.1

What breaks
schema.rb sorts columns alphabetically, so the first migration produces a large one-time diff. Parameter parsing changes: [foo]=bar now parses as the literal key [foo], and semicolons stop acting as query separators. Routing to multiple paths is gone. Benchmark.ms is gone. to_time_preserves_timezone = false is gone. Active Job drops the :never, :always and :default forms of enqueue_after_transaction_commit and the internal SuckerPunch adapter. New deprecations: order-dependent finders such as .first when the relation has no explicit order and the model has no usable implicit_order_column, query_constraints or primary_key fallback (controlled by raise_on_missing_required_finder_order_columns, with removal of the configuration option targeted at 8.2), String#mb_chars, ActiveSupport::Configurable, the built-in sidekiq adapter (sidekiq 7.3.3 or newer supplies its own), insert_all and upsert_all on unpersisted association records, and custom Active Job serializers without a public #klass. Gem lag was real for months: paranoia, globalize and telegram-bot needed vendoring in October 2025; audited was still 7.2-only in April 2026. The bracket parameter change and missing finder ordering both need application-level checks.
What we do
Turn raise_on_missing_required_finder_order_columns on in test first and fix order-dependent finders whose relations and models provide no usable ordering. Primary-key ordering remains a valid fallback; add an explicit order when the application needs a different sequence. Grep query strings and JavaScript for leading-bracket parameter names. Commit the schema.rb reorder as its own no-behavior commit. Upgrade sidekiq before the bump. Land on 8.1.3.1 or later, the only Rails series with security support past 7 November 2026.
What you get
An 8.1.3.1 or later app with the CVE-2026-66066 fix applied and the libvips floor met, a finder-order audit, and a base that is inside security support until 10 October 2027.
Public market range
None published with data as of 4 September 2026. Our own internal applications are on this path now; the per-app logs will be published anonymized and are the only numbers we will quote for this hop.

The Ruby steps

Ruby bumps are separate deploys between Rails hops.

2.7 to 3.0

Keyword-argument separation: every call that warned on 2.7 raises ArgumentError on 3.0, including delegation through *args without ruby2_keywords. $SAFE is removed; webrick, sdbm, net-telnet and xmlrpc leave the standard library; rexml and rss become bundled gems. We turn the 2.7 warnings on and clear them before the switch; old gems with options-hash APIs are where they cluster.

3.0 to 3.3

3.1: Psych 4 makes YAML.load a safe load, so serialized YAML columns, fixtures and config with symbols, dates or custom classes stop loading; net-smtp (Action Mailer's SMTP path) and friends become bundled gems. 3.2: Fixnum, Bignum, Dir.exists?, File.exists? and the taint methods are removed; libyaml is no longer bundled, so the Dockerfile must install libyaml-dev before psych builds. 3.3: racc becomes a bundled gem and Ruby starts warning about the gems that vanish from the default set in 3.4. Inspect the selected Ruby and locked Psych version at the 7.2 rung; the Rails minimum alone does not determine that bundle.

3.3 to 3.4

csv, base64 and bigdecimal, among others, move from default to bundled gems; Prism becomes the default parser; string literals in files without the magic comment warn on mutation under -W:deprecated; Hash#inspect changes to {user: 1}; error messages change format; it becomes a block parameter. Ruby 3.4 also warns about the gems that 4.0 will move, which makes it the second staging stop. Hash#inspect changes can affect exact-string assertions and snapshots; test application compatibility across the other changes too.

3.4 to 4.0

A further set of default gems becomes bundled (ostruct, pstore, benchmark, logger, rdoc, win32ole, irb, reline, readline, fiddle); cgi is reduced to cgi/escape; Set and Pathname become core; the SortedSet autoload is gone; the Ractor messaging API is replaced; *nil no longer calls to_a; backtraces drop internal frames and print Class#method. Frozen string literals are still not the default. ZJIT is off by default and not for production per Ruby core. No Rails release post states which patch level first ran cleanly on 4.0; the gem declares Ruby 3.2.0 or newer. We say runs on Ruby 4.0 only with your app's evidence. One published production report rolled 4.0.2 back to 3.4.9 on latency, so we measure p95 before and after every Ruby bump; a green suite is not enough.

Public market range for Ruby bumps: Rails Fever (22 April 2026) puts them at 1 to 2 days each, an estimate. Not ours.

The image is part of the upgrade

The patched Active Storage code for CVE-2026-66066 requires both libvips 8.13 or newer and ruby-vips 2.2.1 or newer. The distribution versions in the support calendar establish a native-library floor comparison, not an application security result. Record the installed gems and the libvips version loaded by each deployed web or worker process. A package archive or a separate vips command cannot establish all of that state.

Heroku-22 is Ubuntu 22.04 with end of life on 30 April 2027. Buildpacks and application packages can supply a different libvips from the base distribution, so the stack name alone does not establish the loaded library. Ruby 3.2 stopped bundling libyaml, so the image needs libyaml-dev before psych builds.

The advisory documents workarounds with libvips 8.13 or newer. Set VIPS_BLOCK_UNTRUSTED before libvips initializes, or call Vips.block_untrusted(true) when ruby-vips is also 2.2.1 or newer. Below libvips 8.13, it requires removing the application's dependency on libvips. Some applications using ruby-vips only for analysis may be able to remove that gem; verify their dependency and processing requirements first. Inspect the effective processor, including inherited load_defaults settings and explicit overrides. Neither a configuration grep nor a library version establishes whether untrusted uploads are accepted.

The Solid stack is a separate decision

Solid Queue, Solid Cache, Solid Cable, Propshaft and Kamal are defaults for new Rails 8 apps. None is forced on an upgraded app. Each is a scoped project with prerequisites: Solid Queue wants MySQL 8 or newer, MariaDB 10.6 or newer, or PostgreSQL 9.5 or newer for FOR UPDATE SKIP LOCKED and its own database, and reached batches with callbacks in 1.7.0 on 21 August 2026; Propshaft means image_url('hero.jpg') becomes url('/hero.jpg') and the manifest goes away; Kamal means someone owns the OS patching. Heroku's sustaining-engineering model (6 February 2026, no sunset date) is a reason to plan, not a reason to bundle a host move into a version bump. We do not sell an upgrade with a hosting exit attached. We will scope either one on its own.

Tests and CI through the hops, and rollback

7.0 wraps test cases in the executor; 7.1 runs test:prepare and switches DOM testing to HTML5 parsers; 7.2 makes tests honor the queue adapter; 8.1 rewrites schema.rb on the first migration and adds bin/ci with config/ci.rb; Ruby 3.4 and 4.0 change Hash#inspect, Set#inspect and backtrace formats. Both Gemfiles run in your CI for the whole engagement. Deprecation warnings become a CI check, not a log line.

Migrations that cannot reverse are deployed before the bump with a compatibility window; strong_migrations 2.8.0 (14 May 2026) guards the rest. Each version gets its own cache namespace. Cookie and cache formats go in two steps where the guide says two steps. The rollback path is rehearsed and its duration measured in minutes before the first production deploy.

Keep product work compatible during the upgrade

While both framework configurations remain in use, each feature needs to preserve the readers and writers that either configuration still depends on. Run its critical checks under both configurations. Expand the schema compatibly, release the feature, then tighten constraints only after the old path is retired.

In the 20 September 2026 companion experiment, a synthetic invoice feature added a nullable label and a fallback for older records. The old writer still worked; requiring the label too early made its insert fail. The same six component tests passed ten assertions under each of Active Record 8.0.5 and 8.1.3.1. These are recorded experimental baselines, not upgrade-target recommendations.

This AI-assisted example ran each version in a separate process and disposable SQLite database. It did not boot a full Rails application or test a mixed-version deployment. Use it to define a feature compatibility check; validate your application's jobs, cookies, caches and deployment behavior separately.

Download the executable experiments and evidence (ZIP), then open q07.

What you receive

  • An application on Rails 8.1.3.1 or later and Ruby 3.4 or 4.0 with YJIT, inside security support until 10 October 2027
  • Both Gemfiles retired to one; the dual-boot scaffolding removed
  • A per-hop log of what broke and what was done, in the form we keep for our own applications
  • The decision record for every load_defaults setting
  • The gem verdict list with outcomes
  • The front-end and Solid-stack decisions written down with reasons
  • A deploy and rollback path more than one person can run

Frequently asked questions

When the upgrade is the whole job and nothing else needs to change. A single-version bump on a small, well-tested app with current gems is maintenance work, and a Rails maintenance specialist will do it for less than we will. We are the better fit when the upgrade is the starting point rather than the destination: the app is several versions behind, the test suite cannot yet prove a safe cutover, performance or security work rides along with it, or the platform needs to carry new capability afterwards. We will tell you which of those you are in during the assessment, including when the answer is that you do not need us.
From Rails 4.2 through 8.0, to Rails 8.1.3.1 or later on Ruby 3.4, or Ruby 4.0 where your own suite and latency measurements support it. One minor version at a time, each hop its own branch, pull request and deploy, because skipping versions hides the deprecation warnings that tell you what will break next.
Yes. Change Ruby separately from the Rails framework and test the exact locked dependencies under both runtimes. Rails 7.0 declares a Ruby 2.7 minimum, 7.2 declares 3.1, and 8.0/8.1 declare 3.2. These floors do not require deploying obsolete intermediate Rubies or prove every newer Ruby is compatible. Reproduce old combinations in isolation and choose a maintained destination.
Each gets a verdict: upgrade, fork, vendor, replace or delete, with the reason recorded. Vendoring is a bridge, never the destination. Gems block more hops than the framework does, and a gemspec with no upper bound proves nothing; data-migrate broke on 7.1 with activerecord >= 6.1 declared.
No. You choose them from full profiles before kickoff and they stay for the whole engagement; nobody is swapped in behind the scenes. Overlap hours are in US Central.
Yes. The engagement ends at a named state, not at the merge: the app on the target version in production, both Gemfiles retired to one, the rollback path exercised, and the per-hop log handed over.
Anonymized, under NDA. Our own per-app logs from the move of our internal applications to 8.1 are being published as they are written up, and those are the only effort figures we quote.
Not as part of it. Hosting is a question the assessment answers, and when a move is the right answer it is scoped on its own. Heroku's sustaining-engineering model (6 February 2026) names no sunset date, so it is a reason to plan, not a reason to bundle a host move into a version bump.
Check the advisory's affected Active Storage versions together with the application's actual use of libvips and acceptance of untrusted image uploads. Generating variants is not an additional requirement. The advisory lists fixes in 7.2.3.2, 8.0.5.1 and 8.1.3.1, with libvips 8.13 and ruby-vips 2.2.1 or newer required by the patched code. It lists no patched release below 7.2, but provides workarounds for libvips 8.13 or newer and instructions for changing potentially exposed secrets. Unsupported status alone does not establish exposure.
Yes. The old version keeps serving traffic while the new one starts; cache format and cookie changes ship in two steps where the upgrading guide says two steps; database migrations stay backward compatible so both versions can run during the switch; the rollback path is rehearsed and its duration measured before the first production deploy.
Upgrade, in almost every case where the business logic still serves the business. Rewrites restate years of accumulated edge-case knowledge from scratch, and most stall exactly there. The cases where we do recommend rewriting: the domain model no longer matches how you operate, or the application is small enough that a rewrite is genuinely short. Our assessment tells you which case you are in before you commit either way.
As of 6 September 2026, only Rails 8.0.x and 8.1.x remain within their published security-support windows; 8.0.x reaches its cutoff on 7 November 2026. Outside those windows, upstream does not promise new security releases. The policy allows possible fixes in Git without a new release. Check individual advisories for applicability and workarounds, and plan a move to a supported series for ongoing upstream release coverage.

Sources cited on this page

Effort figures attributed to other firms are their published estimates, quoted with date and source; none is an Allerin estimate for any application.

Part of Ruby on Rails consulting at Allerin. Founded 2005. Austin, Texas.

Ready to build your product?

Senior engineering team, measurable outcomes, fast routes to production.

Procurement team? See our Trust Center →