← All posts

How to Rescue a Failing Ruby on Rails App

Legacy codebases don't have to stay broken. Here's the playbook I use to audit, stabilise, and breathe new life into brownfield Rails apps.

Every team inherits a codebase they didn't build. Sometimes that codebase is fine. Sometimes it's a Rails app from 2014 running on Ruby 2.4, with no test coverage, N+1 queries everywhere, and a deployment process that involves SSH-ing into production and running git pull. I've seen both.

If you're reading this, you're probably dealing with the second kind. The good news: Rails apps are remarkably rescuable. The framework's conventions mean there's usually structure to work with — it's just buried under years of accumulated shortcuts. This is the process I follow.

Step 1: Establish a safety baseline before touching anything

The single most dangerous thing you can do with a legacy codebase is start refactoring it without tests. You don't know what's load-bearing. You'll break things and not know it until a customer complains.

Before writing a single line of new code, your first job is to add a test harness around the system's most critical paths — even if those tests are ugly, slow integration tests. They exist to catch regressions, not to be elegant.

Start by identifying the app's revenue-critical flows. Login. Checkout. The API endpoints your mobile app calls. Write high-level request specs for these. You don't need to cover edge cases yet — you just need a smoke test that tells you "did I break the thing that makes money?"

# A rough integration test is infinitely better than none
RSpec.describe "Checkout flow" do
  it "allows a logged-in user to complete a purchase" do
    user = create(:user)
    product = create(:product, price: 49_99)

    sign_in user
    post "/cart", params: { product_id: product.id }
    post "/orders"

    expect(response).to redirect_to(order_confirmation_path)
    expect(Order.last.user).to eq(user)
  end
end

It's rough. It doesn't test every edge case. But it'll catch the worst regressions and give you confidence to move.

Step 2: Run a proper audit — data, not gut feel

Once you have basic safety coverage, run a structured audit before forming an opinion. Gut feel is not a plan.

What to audit:

  • Ruby and Rails versions — Are they EOL? Are they on a supported release? Check ruby -v and bundle exec rails -v against the official support matrix.
  • Gem dependencies — Run bundle outdated. Pay special attention to gems with known CVEs. bundle audit from the bundler-audit gem is your friend here.
  • N+1 queries — Add the bullet gem to development and run through the main flows. You'll almost certainly find queries inside loops that should be includes or preload.
  • Database indexes — Run lol_dba or manually check that every foreign key and common WHERE column has an index. Missing indexes are one of the fastest performance wins in any Rails app.
  • Test coverage — Even a rough SimpleCov report tells you where the risk is. 10% coverage isn't great, but knowing which 10% it is matters.
  • Code quality signalsrubocop with a relaxed config gives you a heat map of complexity. High cyclomatic complexity in a model that also handles billing is a red flag.

Document what you find. This becomes your rescue roadmap and your communication tool with whoever is funding the work.

Step 3: Fix the infrastructure before the code

Application-level problems are usually symptoms of missing operational foundations. Before fixing code, fix the platform:

  • Get proper error tracking in place. Sentry, Honeybadger, or Bugsnag — pick one and wire it up. You cannot fix what you cannot see.
  • Add performance monitoring. Even a free New Relic or Scout APM tier will surface the slowest endpoints and background jobs. The 80/20 rule almost always holds: 20% of your endpoints cause 80% of the pain.
  • Automate deploys. If deployment is a manual process, it's also a risky process. Get to a point where merging to main triggers a deploy. CI/CD removes human error from the chain and makes frequent, small deploys possible.
  • Review database backups. Ask when the last restore test was done. In many teams the answer is "never." Verify that backups actually work before you start major migrations.

These aren't glamorous. They won't show up in a demo. But they're what separates teams that fix things once from teams that fix the same things over and over.

Step 4: Prioritise by impact, not by annoyance

By now you have a list of problems. The temptation is to start with the thing that irritates you most — the massive ApplicationController, the 2000-line model, the god service object. Resist it.

Prioritise by answer to this question: What is currently hurting users or the business?

A slow checkout page costs money every day. A poorly named variable costs nothing. Fix the first kind first.

A practical triage framework:

  1. P0 — User-visible bugs in core flows. Fix these immediately.
  2. P1 — Performance problems causing measurable drop-off. Fix within the first sprint.
  3. P2 — Security vulnerabilities (outdated gems with CVEs, unescaped inputs). Fix before the next release.
  4. P3 — Technical debt that slows down future development. Fix incrementally, alongside feature work.

Step 5: Upgrade Rails and Ruby incrementally

Version upgrades on a legacy Rails app are one of the most misunderstood tasks in the industry. Teams either avoid them entirely (and fall years behind) or try to do them all at once (and break everything).

The right approach: one major version at a time, with tests at each step.

If you're on Rails 5.2 and want to reach Rails 7.1, you don't jump directly. You go 5.2 → 6.0 → 6.1 → 7.0 → 7.1. At each step you run your test suite, fix what breaks, and only move forward once the suite is green. This keeps the change set small and diagnosable.

Use the railsdiff.org tool to see exactly what changed in configuration files between versions. Missing a config change in application.rb is one of the most common sources of subtle bugs during upgrades.

When to bring in outside help

Some rescues are well within the capacity of the existing team, once they have a plan. Others aren't — either because the team is too close to the codebase, too stretched to context-switch, or because they've never done a major Rails version upgrade before.

Signs you might benefit from a specialist:

  • The last Rails upgrade stalled mid-way and was quietly abandoned
  • The team is spending more time firefighting than building
  • There's institutional fear around touching certain parts of the codebase
  • The app is on an EOL Ruby or Rails version and the team doesn't have a clear path forward

A good Rails consultant doesn't just fix the symptoms — they leave the team with a codebase they understand and trust.

Dealing with a struggling Rails app?

I specialise in Rails rescues, audits, and version upgrades. Tell me where things stand and I'll give you an honest assessment of what it'll take to get healthy.

Get in touch