Slow Rails applications aren’t a framework problem; they’re a pattern problem. Most performance bottlenecks stem from a handful of predictable, solvable issues that we’ve been fixing for clients since we started USEO back in 2009.
This isn’t about esoteric tweaks. This is about battle-tested diagnostics and surgical fixes that restore speed and scalability to your application, ensuring it can support business growth for years to come.
Phase 1: The Diagnosis
Performance degradation is rarely a single catastrophic event. It’s a series of small, inefficient cuts that eventually bleed an application dry. Over years of Rails development and rescue projects—from supporting Yousty.ch since 2012 to building new platforms like Versus in 2025—we’ve found that almost all performance issues fall into seven buckets.
The first step is always a thorough diagnosis. We use Application Performance Monitoring (APM) tools and database query analysis to pinpoint the exact source of latency. Often, we find one or more of these culprits:
- N+1 Queries: The most common offender. The app makes one query to fetch a list of items, then makes one additional query for each item in the list to fetch related data.
- Memory Bloat: Loading huge datasets into memory at once. A controller action that tries to instantiate 100,000 ActiveRecord objects will bring most servers to their knees.
- Slow, Complex Queries: A single
ActiveRecordcall that translates into a database query with multiple joins, complexWHEREclauses, and no optimization. The database groans under the weight of the request. - Missing Database Indexes: The database has to perform a full table scan to find data. This is fast on a small table but devastatingly slow on tables with millions of rows.
- No-Strategy Caching: Either a complete lack of caching or, almost as bad, incorrect caching. The application pointlessly re-calculates complex data or re-renders a component for every single request.
- Synchronous Background Jobs: Running a heavy task (like generating a report or sending an email blast) during the request-response cycle. The user is left staring at a loading spinner, waiting for the server to finish.
- View Layer Bloat: Complex logic, database queries, and object formatting happening directly inside the
.html.erbor.html.hamltemplates. This makes views slow to render and impossible to test.
Synthetic Engineering Context: Diagnosing Inefficiencies
When a user reports “the dashboard is slow,” our first instinct is to look at the logs for the controller action rendering that page. We often find code like this, a classic N+1 example where we’re displaying a list of articles and their authors.
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article.order(created_at: :desc).limit(20)
end
end
# app/views/articles/index.html.erb
# <% @articles.each do |article| %>
# <h2><%= article.title %></h2>
# <p>by <%= article.author.name %></p>
# <% end %>
# LOG OUTPUT:
# SELECT "articles".* FROM "articles" ORDER BY "articles"."created_at" DESC LIMIT 20
# SELECT "authors".* FROM "authors" WHERE "authors"."id" = 1 LIMIT 1
# SELECT "authors".* FROM "authors" WHERE "authors"."id" = 23 LIMIT 1
# SELECT "authors".* FROM "authors" WHERE "authors"."id" = 15 LIMIT 1
# ... (19 more queries)
The log file doesn’t lie. One query for articles triggers 20 more queries for authors. This is the smoking gun.
Phase 2: The Fixes
Once diagnosed, the fixes are often straightforward and elegant. Rails provides a powerful toolkit for resolving these common issues, but it requires the discipline to use it correctly. The goal isn’t just to make the code work, but to make it work efficiently at scale.
For each of the problems identified in Phase 1, there is a corresponding, well-established solution:
- For N+1 Queries: Use
ActiveRecord::QueryMethods#includesto eagerly load associated records in a single query. - For Memory Bloat: Don’t load everything at once. Use
ActiveRecord::Batches#find_eachto process records in batches, or useActiveRecord::Calculations#pluckto select only the specific columns you need, bypassing the overhead of creating full model objects. - For Slow Queries: Use a tool like
EXPLAINto analyze the query plan. Often, the solution is a combination of adding an index and refactoring the query to be simpler. - For Missing Indexes: Add indexes to foreign keys and any columns frequently used in
WHEREclauses. This is often the single most effective performance improvement you can make. - For Caching: Implement a coherent caching strategy. Use a dedicated caching store like Redis. Cache low-level model data, fragment cache expensive view components, and use Russian Doll Caching to manage dependencies.
- For Sync Jobs: Move all non-critical, long-running tasks to a background job processor like Sidekiq. The controller should return a response to the user immediately, while the heavy lifting happens on a different thread.
- For View Bloat: Move logic out of views and into dedicated Presenter or Decorator objects. Keep your views dumb and your logic contained.
Synthetic Engineering Context: Applying Targeted Fixes
Applying the fix for our N+1 example is a one-line change that has a massive impact. By telling ActiveRecord to includes the author association, we reduce 21 database queries to just 2.
# app/controllers/articles_controller.rb (The Fix)
class ArticlesController < ApplicationController
def index
@articles = Article.includes(:author).order(created_at: :desc).limit(20)
end
end
# LOG OUTPUT (AFTER FIX):
# SELECT "articles".* FROM "articles" ORDER BY "articles"."created_at" DESC LIMIT 20
# SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (1, 23, 15, ...)
Similarly, if we only needed author names, we wouldn’t even need includes. We could use pluck to get just the data we need, which is even more efficient.
# More efficient if you only need one column
author_names = Author.where(id: [1, 23, 15, ...]).pluck(:name)
# => ["Dariusz Michalski", "John Doe", "Jane Smith"]
Phase 3: Monitoring & Prevention
Fixing existing problems is only half the battle. A mature engineering organization builds systems to prevent performance regressions from happening in the first place. This means establishing a culture of performance awareness, backed by automated tooling.
Your CI/CD pipeline is your first line of defense. Every pull request should be automatically checked for new N+1 queries or memory usage spikes.
Key tools and practices for prevention include:
- Application Performance Monitoring (APM): Integrate a tool like Scout APM or Skylight into your production and staging environments. These tools provide real-time dashboards that surface slow endpoints and inefficient queries before they become critical.
- Performance Regression Tests: Write automated tests that assert the performance characteristics of critical endpoints. A test can fail a build if a page suddenly takes 500ms longer to load or makes 10 more database queries than it did before.
- Regular Audits: Schedule periodic performance and technical debt audits to proactively identify and prioritize fixes before they impact users.
Synthetic Engineering Context: Building a Performance Culture
A performance test in your CI pipeline can be as simple as a test that fails if an unexpected number of queries are made. Using a gem like test-prof, we can write a test that codifies our performance expectations.
# spec/requests/articles_spec.rb
require 'rails_helper'
RSpec.describe "Articles", type: :request do
describe "GET /articles" do
it "does not have N+1 queries", :n_plus_one do
# Create some articles with authors
authors = create_list(:author, 2)
create(:article, author: authors.first)
create(:article, author: authors.second)
# Expect the page to load with a specific number of queries
# This will fail if the controller is not using .includes(:author)
expect { get articles_path }.to perform_under(3).queries
end
end
end
When this test runs in CI, it prevents a developer from accidentally merging a change that re-introduces the N+1 problem. This is how you build a fast, maintainable application for the long term.
Need help with Rails performance?
If you’re facing these issues, you’re not alone. We’ve spent over a decade helping companies of all sizes diagnose and fix Rails performance bottlenecks.
Our Technical Debt Audit is the first step toward a faster, more reliable application. We’ll give you a concrete, prioritized roadmap for reclaiming your app’s performance.