
How to Speed Up Your Rails Backend Server
Victor Motogna
Head of Web Development
Reading time: 5 min
Published: Jun 10, 2021
Key takeaways
- The N+1 query problem is the most common cause of a slow Rails backend, running one extra query per record.
- N+1 queries are hard to spot because each individual query runs fast and slips past benchmarking tools.
- In Rails, the includes method eager-loads associations and collapses N+1 queries into just two queries.
- Use references and joins alongside includes, depending on whether you need to render the associated data.
- Catch N+1 queries automatically with the Bullet gem in your test and CI environments; benchmarks show 2-4x speedups.
Many small things can slow down a Rails backend server. We cannot cover them all in one article. So today we focus on the most common culprit: the N+1 query problem.
What is an N+1 query?
Let's start with the basics. What is an N+1 query, and why is it a problem?
An N+1 query is a common mistake that can slow your app badly. It happens when your code runs N extra queries to fetch data that the first query could have loaded. The larger the N, the slower your app gets.
Here is a simple example. Say you have a one-to-many relationship and you want all parent records with their children. A naive query loads all the parents first. Then it runs one more query per parent to load the children.
This creates a flood of queries. Each one adds latency and drags down Ruby on Rails performance. Worse, they are hard to spot. Every single query runs fast, so benchmarking tools often miss them.
How do you fix N+1 queries?
The general fix, in any framework, is to use JOINS. A join pulls the lazy-loaded child records in the first query. So you avoid one extra query per record.
Most frameworks also offer libraries that detect N+1 queries or eager-load associations for you. Rails is no exception.
Our example setup
For the rest of this guide, we use a simple Ruby on Rails example. It is prone to N+1 queries, but easy to fix. We have a Parent class and a Child class in a one-to-many relationship.
# models/parent.rb
class Parent < ApplicationRecord
has_many :children
end
# models/child.rb
class Child < ApplicationRecord
belongs_to :parent
end
The Rails way: includes
As noted, joins are a good fix. Luckily, Rails and Active Record give us a great method for this: includes. It eager-loads the associations you name.
In a serializer and controller, natural code looks like this:
# serializers/parent_serializer.rb
class ParentSerializer < ActiveModel::Serializer
attributes :id, :string_field
has_many :children
end
# controllers/parents_controller.rb
def index
render json: Parent.all
end
The serializer also renders each parent's children. Because of how Active Record works, this runs a new query for every parent.
Our database held 21 Parent records. This logic ran a fresh query for each one. Now imagine 10,000 records.

The simplest fix adds just a few characters:
# old: render json: Parent.all
render json: Parent.includes(:children).all
Now the log looks very different:

This is a clear win. The logs now show 2 queries instead of N+1: one to load the parents, and one to load all their children.
How does includes work?
The includes method works in two ways, based on the query. Most of the time it uses preload. In some cases it uses eager_load. Active Record picks preload by default, unless the association also appears in another clause, such as where.
The nice part is flexibility. You can preload several relations at once with Parent.includes(:child1, :child2, :child3). You can also load nested associations with Parent.includes(child: :grandchild), and even deeper.
Other options: references and joins
Rails and Active Record give you three main methods for handling associations:
- includes preloads associated models to avoid N+1 queries. Use it when you plan to render those models.
- references works with includes. It forces the tables to be joined rather than loaded separately.
-
joins is for when you need an associated table in a
whereororderclause, but don't need to render it.
How to catch N+1 queries
We don't think about N+1 queries on every line of code. And we shouldn't. That is what tools are for.
You can also run an eye test. Debug a slow controller action and watch how many queries it fires. The problem is usually obvious.
For an easier way, many libraries can help. The one we use most is Bullet, the most popular Rails option. The basic setup takes only a few lines:
config.after_initialize do
Bullet.enable = true
Bullet.bullet_logger = true
Bullet.raise = true # fail the test if an N+1 query occurs
end
We add this to the test environment. That way, we catch issues when running tests locally and in CI. Bullet also supports many options, such as whitelisting associations so valid cases don't fail tests.
Does it matter? The benchmarks
There are many benchmark resources on N+1 queries already. Here we ran a quick local test to show how much even a few records can slow things down.
We used the same Parent and Child models. We tested 20, 100, and 1,000 parent records. We compared the naive version (with N+1 queries) against the version using includes. To make it realistic, each test had ten times more children than parents:
FactoryBot.create_list(:parent, number)
FactoryBot.create_list(:child, number * 10, parent_id: Parent.all.pluck(:id).sample)
Here are the results:
-
20 parents, 200 children (about 4x slower): 540ms without
includes, 136ms with it. -
100 parents, 1,000 children (about 2x slower): 716ms without
includes, 339ms with it. -
1,000 parents, 10,000 children (about 2x slower): 3.38s without
includes, 1.85s with it.
The takeaway is clear. Fixing N+1 queries is one of the easiest wins for Ruby on Rails performance. And it costs you only a few extra characters.
Speed up your Rails app with Wolfpack Digital
Fast backends make happy users. Fixing N+1 queries is just one step. Want a Rails app that scales? Learn why we build with Ruby on Rails, or read our tips on secure Rails apps.
You can also see how our web development team works, or dig into the official Active Record Query Interface guide. Ready to build? Get in touch.



