// // insight

Upgrading an old Rails app: how to plan it

Upgrading an old Rails app requires auditing gem dependencies, raising test coverage above 75%, and stepping incrementally through minor versions (5.2 to 6.0 to 6.1 to 7.0) rather than making a single major jump. Dual-booting via environment variables lets engineers run the modern Rails version in CI alongside production, fixing deprecation warnings in main without freezing feature development.

Published October 3, 2026 · Reviewed by the NextGen engineering team

Planning a Rails app upgrade requires auditing gem dependencies, establishing test coverage above 80%, and stepping incrementally through minor versions (e.g., 5.2 to 6.0 to 6.1 to 7.0) rather than attempting a single major leap. Use dual-booting via environment variables to run new Rails versions in CI alongside your current production environment before switching traffic.

Why legacy Rails upgrades get stuck (and how to unblock them)

Most legacy Rails upgrades die in committee. The engineering team knows the application needs to get off Rails 4.2 or 5.2, but product leadership refuses to freeze feature development for four months to rewrite code that already works in production.

The stall happens because teams treat an upgrade like a single, massive rewrite rather than a continuous engineering pipeline. When you try to branch off main, upgrade 40 gems, change the Ruby runtime, and fix 800 broken tests in isolation, your feature branch dies of git merge conflicts within three weeks.

Upgrading an application is not about writing new code. It is an exercise in risk reduction, dependency resolution, and test harness stabilization. You can upgrade a enterprise application while your product team continues shipping revenue-generating features every week.

The version hopping path: Never skip major versions

The fundamental rule of upgrading Ruby on Rails is simple: you must resolve all deprecation warnings on the latest minor version of your current release before stepping to the next major version.

If you are on Rails 5.2, you cannot jump directly to Rails 7.1. You must get to 5.2.x, fix every warning, upgrade to 6.0, clear warnings, upgrade to 6.1, clear warnings, upgrade to 7.0, and finally reach 7.1. Skipping versions skips the framework's built-in deprecation warnings, turning explicit log guidance into silent production failures.

Rails 5.2  --->  Rails 6.0  --->  Rails 6.1  --->  Rails 7.0  --->  Rails 7.1 / 8.0
(Fix warnings)   (Fix warnings)   (Fix warnings)   (Fix warnings)   (Target)

Each step in this chain validates three things:

  1. Gem compatibility: Every gem in your Gemfile supports both the old and new target versions.
  2. Ruby runtime support: Your Ruby version matches the matrix required by the target framework.
  3. Framework conventions: Your code replaces deprecated methods (like update_attributes or manual autoloading) with modern equivalents.

Upgrade matrix: Timelines, Ruby versions, and breaking changes

The following matrix breaks down the typical path for modernizing legacy applications. Timelines assume an application with 50k to 200k lines of code and moderate test coverage.

Upgrade StepTarget Ruby VersionTypical TimelineMajor Landmines to Watch
4.2 to 5.0 / 5.2Ruby 2.5 -> 2.74 to 8 weeksActionController parameters (no longer a Hash), ActiveRecord belongs_to required by default, MySQL/Postgres SSL defaults.
5.2 to 6.0 / 6.1Ruby 2.7 -> 3.04 to 6 weeksAutoloading shift (Classic to Zeitwerk), Sprockets to Webpacker/Propshaft, keyword arguments in Ruby 3.0.
6.1 to 7.0 / 7.1Ruby 3.0 -> 3.2+3 to 6 weeksEncrypted attributes migration, Sprockets deprecation, Psych YAML parsing changes, enum syntax updates.
7.1 to 8.0Ruby 3.2 -> 3.3+2 to 4 weeksAsset pipeline migration (importmaps/Tailwind), Kamal deployment defaults, solid_queue replacing Redis/Sidekiq.

The dual-booting pattern: Upgrading without freezing features

The cleanest way to handle a major upgrade is dual-booting. Dual-booting allows your development environment and CI server to run your application using either the current Rails version or the target Rails version based on an environment variable.

You do not need to maintain a separate, long-lived git branch. Instead, add a conditional check to your Gemfile:

## Gemfile
source "https://rubygems.org"

def next_framework?
  ENV["RAILS_NEXT"] == "1"
end

if next_framework?
  gem "rails", "~> 7.0.0"
  gem "psych", "~> 5.0"
else
  gem "rails", "~> 6.1.7"
end

Using this pattern, engineers work on normal product features using RAILS_NEXT=0 (the default). A dedicated upgrade engineer or secondary CI pipeline runs tests daily using RAILS_NEXT=1.

When a test breaks under the new version, you fix it in main using version checks or gem updates that work across both versions. Once the RAILS_NEXT=1 CI build passes cleanly across the entire suite, you flip the default environment variable, remove the conditional logic, and deploy.

Clearing the technical landmines

Every major Rails release introduces architectural shifts. Ignored during minor upgrades, these four areas account for 80% of application failures:

1. The Zeitwerk Autoloader Migration (Rails 6.0)

Rails 6 deprecated the Classic autoloader in favor of Zeitwerk. If your application relies on custom file structures, non-standard class names, or explicit require calls inside app/, Zeitwerk will throw initialization errors. Run bin/rails zeitwerk:check early to surface naming mismatches before attempting the 6.0 hop.

2. Ruby 3.0 Keyword Argument Separation

Ruby 3.0 separated positional arguments and keyword arguments. If your Rails app calls methods like redirect_to(url, options), Ruby 3.0 will throw a ArgumentError (wrong number of arguments). Upgrade your Ruby version to 2.7 first, resolve all keyword argument warnings, and only then proceed to Ruby 3.x.

3. Database Column Defaults and Active Record Enums

Rails 7.0 changed how enums and database defaults interact. If your database schemas rely on raw SQL defaults or implicit enum mappings, models may load invalid states. Audit your schema.rb or structure.sql for custom database types before touching model code.

4. Third-Party Gem Abandonment

The hardest part of a Rails upgrade is rarely Rails itself—it is unmaintained gems. If your application relies on a gem that hasn't seen a commit since 2016, you have three options:

  • Fork the gem and fix the incompatibilities yourself.
  • Replace the gem with a standard Rails core feature or maintained alternative.
  • Monkey-patch the gem inside your application initializers as a temporary bridge.

Staffing math: Internal engineers vs dedicated specialized teams

Upgrading an application requires pulling engineers off feature development. The hidden cost isn't just the contract spend; it is the opportunity cost of delaying your core roadmap.

Consider an in-house team of four senior developers earning an average base salary of $175,000 each.

In-House Cost Calculation:
4 Engineers * $175,000 Base Salary = $700,000 Total Annual Payroll
Monthly Payroll = $700,000 / 12 = $58,333

3-Month Upgrade Duration:
$58,333 * 3 Months = $175,000 Direct Payroll Spend
+ Lost Opportunity Cost of 3 Months of Zero Product Feature Releases

When internal engineers perform upgrades, they do it context-switching between sprint tasks and framework bugs. Progress slows down because they encounter framework deprecation patterns once every few years.

Bringing in a dedicated US-based Ruby on Rails developer or specialized team shifts this equation. A dedicated two-person staff-level engineering pod can complete an upgrade from Rails 5.2 to 7.1 in 8 to 12 weeks for a typical $120,000 to $180,000 flat scope. Your internal team keeps shipping product features, and the upgrade pod handles dependency resolution, CI updates, and gem modernizations.

A step-by-step checklist to start your upgrade

Before writing code or hiring help, run this checklist against your repository:

  1. Audit test coverage: Run a coverage tool like SimpleCov. If coverage is below 75%, write integration tests for critical business paths (checkout, authentication, billing) before touching dependencies.
  2. Update to the latest patch release: Ensure you are running the absolute latest patch version of your current Rails release (e.g., move from 5.2.1 to 5.2.8.1).
  3. Audit your Gemfile: Run bundle outdated. Identify abandoned gems, unpinned dependencies, or gems pointing directly to custom git branches.
  4. Fix deprecation warnings: Enable deprecation logging in your development environment by adding config.active_support.deprecation = :log to config/environments/development.rb. Search logs for DEPRECATION WARNING and fix them.
  5. Establish a dual-boot CI pipeline: Configure your build tool (GitHub Actions, CircleCI) to run a parallel build with the target Rails version turned on.

What this means for your team

An outdated Rails version is an operational risk. It exposes your infrastructure to security vulnerabilities, prevents you from utilizing performance improvements in modern Ruby runtimes, and makes recruiting senior talent difficult. Nobody wants to write feature code on a decade-old software stack.

Upgrades do not require stopping business operations or risking production outages. By applying incremental version hops, establishing dual-boot CI pipelines, and systematically eliminating gem rot, you turn a terrifying system overhaul into a predictable engineering task.

If your team is facing an outdated Rails codebase, missing test coverage, or an unmaintained gem dependency tree, we can help. Talk to senior staff engineers who have modernized legacy Rails systems for over a decade. Reach out to our engineering team to scope your upgrade timeline and get a flat-fee assessment.

Frequently asked

How long does a typical Rails app upgrade take?
A standard upgrade for an application with 50k to 200k lines of code typically takes between 8 to 16 weeks depending on gem compatibility and existing test coverage. Step-by-step minor version hops generally require 2 to 4 weeks of engineering effort per major framework version.
Should you rewrite or upgrade an old Rails application?
Upgrading is almost always faster, cheaper, and less risky than a complete ground-up rewrite. Rewriting introduces massive business logic gaps and regression risks, whereas incremental upgrades preserve working production features while modernizing the underlying software stack.
Can you upgrade Rails without stopping new feature development?
Yes, dual-booting allows your engineering team to continue shipping product features while systematically resolving deprecation warnings for the target Rails version. By using environment variables in your Gemfile, your CI pipeline runs daily builds on the new version while main remains stable.
What is dual-booting in Ruby on Rails?
Dual-booting is a configuration technique where your Gemfile conditionally loads different framework and gem versions based on an environment variable like RAILS_NEXT. This allows CI pipelines and local environments to run tests against a future Rails version while your main codebase targets the active production version.
What is the risk of skipping major Rails versions during an upgrade?
Skipping major versions bypasses the framework's built-in deprecation warnings, turning explicit log guidance into silent runtime failures and broken database queries. You must step through each release sequentially to identify deprecated methods before they are removed from the framework.

More answers in Insights or see AI development services.

// let's build something

Start your project request

Tell us what you're building — engineering capacity, AI, QA, cloud, or a fixed-scope software engagement. Our NYC team responds within one business day.

// what to expect
  • Response within 1 business day
  • 30-minute discovery conversation
  • Recommended engagement model & pricing
  • NYC-focused — in-person available
Start Project Request

Inbound sales only. All form information is encrypted in transit.