Many successful companies run on Rails applications that are five or even ten years old. These applications work. They make money. But they are often stuck on an old version of Rails. The thought of upgrading feels overwhelming.
Your team might be tempted by a full rewrite. A rewrite promises a clean slate but it is almost always a mistake. It underestimates the complexity of the old system and introduces new unknown risks. The other option a big bang upgrade where you try to jump multiple major versions at once is just as dangerous.
There is a third way. A slower more deliberate and much safer way. It is the path of incremental upgrades.
The first thing to understand is that the goal is not simply to be running the latest version of Rails. The real goal is to get your application into a state where upgrading is no longer a scary multi quarter project. The goal is to make small regular upgrades a normal part of your development cycle.
When you are three or four major versions behind you are in a legacy trap. Your team avoids dependencies because they might not be compatible. You cannot use new features that would simplify your code. Security patches become harder to apply. The point of this first big upgrade is to escape that trap forever.
You cannot upgrade a complex application without confidence. And in software confidence comes from a good test suite. Before you change your Rails version you must be able to verify that the application still works as expected.
This does not mean you need 100 percent test coverage. That is an unrealistic goal for most legacy projects. Instead focus on the critical paths. Can a user sign up? Can they complete a purchase? Does your most important background job run correctly? Make sure these core flows are covered by reliable tests.
If your test suite is slow it will become a source of friction. A slow test suite discourages you from running it which defeats its purpose. You might find you need to fix your tests before you can even begin. If you’re running into this problem a good place to start is understanding why your Rails tests are slow.
The single most important rule of incremental upgrading is this go one minor version at a time. Do not jump from Rails 4.2 to Rails 7. Your path should look like 4.2 to 5.0 then 5.0 to 5.1 then 5.1 to 5.2 and so on.
This sounds tedious but it is the key to minimizing risk. Each minor version jump introduces a smaller set of changes. The deprecation warnings are more specific. The blog posts and guides for that specific jump are easier to find. When a test breaks you have a much smaller surface area to investigate.
It transforms a massive unknown problem into a series of small manageable ones. You can often complete a minor version upgrade in a day or two. This provides momentum and a sense of progress for the team.
A practical challenge is how to work on the upgrade without disrupting the work of the rest of the team. You do not want to maintain a long lived upgrade branch that is impossible to merge. The solution is a dual boot setup.
You can configure your Gemfile to use a different set of gems based on an environment variable.
if ENV['RAILS_NEXT'] gem 'rails', '~> 5.1.0' # other updated gems else gem 'rails', '~> 5.0.0' # original gems end
With this in place a developer can run the application or the test suite against the next version of Rails by simply setting an environment variable.
RAILS_NEXT=1 bundle exec rspec
This allows you to fix deprecations and failing tests on the main branch. You commit small fixes incrementally. When the test suite is fully green under the RAILS_NEXT
flag you can flip the switch in the Gemfile for everyone merge it and you are done. Then you start the process over for the next minor version.
The upgrade process is fundamentally a loop. First you run the tests with the next version. Then you read the deprecation warnings and failures. These warnings are your roadmap. They are the Rails core team telling you exactly what you need to change.
Fix one deprecation or one failing test. Commit the change. Rerun the tests. Repeat. This is not glamorous work. It is a methodical process of following instructions. Do not try to refactor other code or make unrelated improvements during this process. The goal is to change as little as possible to make the tests pass on the new version.
As you work through the tedious process of fixing deprecations the idea of a rewrite will seem appealing. It is a siren song. A rewrite feels like heroic work while an incremental upgrade feels like plumbing.
But a successful product has years of bug fixes and edge cases encoded in its codebase. A rewrite throws all that learning away. It is almost guaranteed to take twice as long as you estimate and launch with a fraction of the stability of the original.
Stick with the upgrade. It honors the work that has come before. It delivers value to users and the business much faster. It is the professional path. The upgrade gets you to a modern stack from which you can then begin to refactor and improve the application with confidence.
Upgrading a legacy app is a test of patience and discipline not cleverness. Try thinking about your own projects for a moment. What’s the oldest application you still maintain?
— Rishi Banerjee
September 2025