A Rails 8 Upgrade Story: Building Momentum Without a Rewrite
Reading time: ~ 8 minutes
Evolving your Rails app isn’t about starting over — it’s about writing the next chapter with confidence.
If you have a Rails app, 5+ years old, it can start to feel like a ticking time bomb. When you log into the codebase and see outdated gems, warnings, and features that were written by developers who left long ago, you might start thinking: “We’re too far behind. Maybe we should just rebuild from scratch.”
It’s a common reaction, but rewrites often end up being more expensive, more disruptive, and slower to deliver results than they expected. Also, legacy apps still hold the heart of your business—core logic, customer data, integrations, and workflows that deserve care, not replacement.
The good news? You don’t have to start over to prepare for Rails 8. You can evolve what you already have—modernizing step-by-step while preserving the value built over the years. That’s the kind of stewardship we bring to projects: helping good software find its second act.
Most recently, we helped a client modernize their Rails 5.2 application, upgrading it incrementally to Rails 7.1 with a clear, low-risk path to Rails 8. The app stayed online, the business kept running, and the team found a renewed confidence in their codebase. Here’s how we did it.
The Problem: When Staying Current Feels Out of Reach
Legacy Rails apps have been around long enough to prove their business value, but also long enough to accumulate complexity. Over time, what was once cutting-edge starts to feel complex or fragile. You might recognize a few of these patterns:
- Your app is 7+ years old, with contributions from multiple developers over the years.
- Gems are frozen in time, some untouched for years.
- Technical debt is layered like sediment, easy to ignore, until it slows you down.
- Brittle code "just works," making changes feel risky.
- There’s a competitive pressure from newer, faster-moving apps.
Our client was facing all of these scenarios. Their Rails 5.2 application had been powering their business reliably for years. Customers depended on it daily, but the release of Rails 8 created urgency. They knew falling further behind could put them at risk; yet the idea of a full rewrite seemed overwhelming: high costs, a long timeline, and huge disruption to their users.
The question they brought to us was: “Can we move forward without starting over?”
The Approach: Audit First, Panic Never
The worst way to tackle a Rails upgrade is to jump in blind, changing versions and hoping tests catch the fallout. That’s where fear comes from—teams don’t know what’s hiding in their codebase. Our philosophy is simple: upgrades aren’t chores, they’re opportunities to gain clarity.
So we began with a comprehensive technical audit. Instead of guessing where the risks were, we mapped the entire ecosystem of the app: dependencies, security profile, performance issues, and tolerance for change. That perspective helped shift the team from feeling stuck to feeling in control of their app’s future.
What We Audited
We looked at the application from multiple angles:
- Ruby version compatibility. Which version was it running? What versions stood between the client’s Ruby 2.6 baseline and the modern 3.2 we’d need for Rails 8+? Each jump had to be planned carefully to avoid gem breakage.
- Gem dependencies. We identified gems that were dangerously out of date, incompatible with Rails 6/7, or abandoned entirely. These were potential roadblocks and needed replacement strategies.
- Test coverage. We assessed how much of the app had a safety net. Where gaps existed, we wrote tests for critical business logic first, ensuring upgrades wouldn’t silently break revenue-generating features.
- Security vulnerabilities. We ran scans to detect risks hiding in outdated dependencies and unsanitized code paths. An upgrade is the perfect time to close those doors.
- Performance issues. Using profiling tools, we flagged places where the app was already slow so that upgrades wouldn’t make the problem bigger.
- Deprecated patterns. Rails is famous for clear deprecation paths, but only if you catch them early. We hunted down methods and configurations that would break as soon as Rails 6+ was introduced.
Our Audit Tools (and Why They Mattered)
We integrated a suite of tools into a CI pipeline, each targeting a specific risk area, giving developers and stakeholders a clear picture of the app’s state.
bundle outdated → Dependency Risk
This tool lists outdated gems, highlighting security patches and compatibility issues. We ran bundle outdated --strict in our CI pipeline, cross-referencing with CVE databases to prioritize critical updates. This prevented production failures by catching compatibility issues early, saving weeks of debugging.
Brakeman → Security Risk
Brakeman scans Rails code for vulnerabilities like SQL injection or XSS. We configured it with a custom ignore file to filter false positives, running it pre-upgrade to harden the app. Addressing vulnerabilities early meant the upgrade improved security, not just compatibility, reassuring stakeholders about data safety.
RubyCritic → Maintainability Risk
RubyCritic scores code quality, flagging complexity (Flog scores >30) and code smells. We used it to prioritize refactoring before upgrades. This made the codebase easier to maintain, reducing future technical debt for developers.
Performance Profiling Tools → User Experience Risk
Tools like Bullet and Rack Mini Profiler, run in staging, identified bottlenecks like N+1 queries. We baselined performance to ensure upgrades didn’t degrade user experience. Faster pages improved user satisfaction, directly supporting business goals like retention.
By combining these tools in a staged pipeline—dependencies first, then security, maintainability, and performance—we eliminated blind spots, making a no-rewrite upgrade feasible.
The Plan: Mapping a No-Rewrite Upgrade Strategy
We designed an incremental roadmap, splitting the upgrade into four phases. Each ensured stability for thousands of users while targeting Rails 8 readiness. This wasn’t a rescue mission—it was a roadmap for evolution.
Each phase built confidence and reduced risk, proving that modernization doesn’t have to mean disruption. Here’s a brief summary of each phase and how we worked through our plan.
Phase 1: Foundation work- secure the app, save debugging time, and ensure uptime
- Upgraded Ruby from 2.6 to 3.2, testing with rbenv (e.g., fixed nokogiri issues).
- Updated gems like Devise via bundle outdated for Rails 7 compatibility.
- Fixed Brakeman XSS vulnerabilities with strong_parameters.
- Raised payment logic test coverage from 60% to 85%.
Phase 2: Rails minor upgrades- clear deprecations and ease major upgrades
- Patched to Rails 5.2.8 for security.
- Upgraded to Rails 6.0, then 6.1, fixing where.not queries for NOR-to-NAND deprecation.
- Updated database.yml for multiple databases.
Phase 3: Major Rails upgrade- modernize infrastructure and increase efficiency
- Upgraded to Rails 7.0 and then 7.1, fixing Zeitwerk autoloading issues by aligning file names and class/module names (e.g.,
user.rb→class User, rather than mismatched names likeuser_model.rb→class User).. - Switched to Propshaft, resolving asset 404s.
- Adopted importmap-rails, rewriting jQuery to Stimulus.
Phase 4: Rails 8 preparation- future-proof for Rails 8 and maintain performance
- Tested Rails 8 beta features in staging, including Solid Queue for built-in background job processing.
- Ensured gem compatibility (e.g., tested Devise’s main branch) with bundle update.
- Cached report queries; documented Turbo streams for onboarding.
Prioritization Framework
We scored tasks on three factors:
- Impact: Did it improve performance, security, or developer productivity?
- Effort: How many developer hours or complexity was involved?
- Risk: Could it cause regressions or downtime?
High-Impact, low-Effort tasks (e.g., Ruby 3.2 upgrade) came first, while high-Risk tasks (e.g., ActiveRecord refactoring) required extra tests. For example, upgrading Ruby scored high for Impact (unlocked Rails 7 features) and moderate for Effort (gem updates), with low Risk due to test coverage. When gem conflicts delayed non-critical features, we deferred them to later phases, balancing speed with stability.
Tricky Areas We Encountered
- ActiveRecord Changes: Rails 6’s where.not deprecation (NOR to NAND) broke report queries (e.g., Post.where.not(source_type: "Feed", source_id: 100)). We refactored to chained where.not calls (e.g., Post.where.not(source_type: "Feed").where.not(source_id: 100)), ensuring data accuracy.
- Gem Conflicts: will_paginate wasn’t Rails 7-compatible. We switched to pagy, rewriting views but boosting memory efficiency.
- Deprecated Code: Rails 5.2’s ActionController deprecations (e.g., old render syntax) became Rails 6 errors. deprecation_toolkit in CI helped fix most proactively.
- Sprockets Fingerprinting Changes: Rails 6.1’s regex broke non-standard assets (e.g., image-1.2.3.js), causing 404s. We set config.assets.unknown_asset_fallback = true temporarily and renamed assets, avoiding user-facing errors.
The Outcome: Rails 8-Ready, No Rewrite Required
Wins!
By the end of the project, the client’s application was running confidently on Rails 7.1 with a clear path to Rails 8—delivered without a rewrite or downtime. Over the course of three months, we preserved service for thousands of daily users while steadily modernizing the codebase. The upgrade paid off quickly: dashboards that once took nearly four seconds to load now render in under two, test suites that dragged for fifteen minutes now finish in five, and every critical vulnerability uncovered in the audit was patched.
Just as importantly, the app now follows current Rails conventions, making onboarding new developers faster and less painful. The team regained trust in their own system. What once felt like a fragile legacy app now feels like a foundation for growth—a true second act.
Business Impacts
The upgrade translated into less time spent firefighting and more time building. With fewer production issues, the client’s developers could focus on delivering new features rather than patching old ones. Modern Rails patterns and faster test runs shortened release cycles from weeks to days, giving the company a real competitive edge. Most importantly, by avoiding a costly rewrite, the client preserved their budget while still securing a platform that’s faster, safer, and ready to adopt Rails 8 features when the time comes.
Client Feedback
We thought a rebuild was our only option. Instead, we modernized incrementally while keeping our business running. The process was far less disruptive than we feared."
How You Can Prepare for Rails 8, Too
- Don’t rewrite—review: Audit your app to uncover strengths and risks. Much of your “legacy” code carries business value worth preserving.
- Audit before you act: Use tools to map dependencies, security, and performance before planning.
- Break work into chunks: Incremental upgrades deliver value safely, phase by phase.
- Partner with experience: Choose a team that’s helped other organizations modernize without hitting pause on their business.
- Connect upgrades to business value: Tie every step to tangible outcomes: faster performance, stronger security, happier users.
Every Rails app that’s stood the test of time has a story worth continuing.
Rails 8 is just the next chapter. With the right plan and the right partners, your app’s second act can be even stronger than the first.
We Also Think You’ll Enjoy Reading…
When Should You Upgrade Your Rails Application?
Ruby on Rails Code Audits: 8 Steps to Review Your App
Helpful Resources for Upgrading Your Rails App Version
FAQ’s
Q. Do I need to rebuild my Rails app for Rails 8?
Not at all. In most cases, a full rewrite isn’t necessary. You can modernize your existing app through deliberate, incremental upgrades that keep your business running while preparing for the future.
Q. What’s the benefit of upgrading instead of starting over?
Upgrading lets you build on the foundation you already have—your code, your team’s knowledge, and your customers’ trust. A rewrite often resets all of that, while an upgrade preserves your investment and adds new capabilities.
Q. How long does a Rails 8 upgrade take?
Every app is different. We usually recommend starting with a short technical audit to identify dependencies, risks, and opportunities. From there, we map out a phased plan—often spanning a few weeks to a few months, depending on complexity.
Q. What version of Ruby should I be running for Rails 8?
Rails 8 requires Ruby 3.2 or higher. If your app is still on Ruby 2.x, upgrading Ruby is an essential first step—and one that brings immediate performance and security improvements.
Q. How do I know if my app is ready for Rails 8?
If your app is running on Rails 6 or 7 with recent Ruby and gem updates, you’re in great shape. If you’re several versions behind, a readiness audit will clarify what needs attention first.
Q. Can we stay on an older version instead?
You can—but it becomes more expensive and risky over time. Unsupported versions stop receiving security patches, making upgrades harder later. Incremental updates keep your app healthy and adaptable.