Purple collage of railway tracks merging into server racks, titled How to Speed up your Rails Backend Server

How to Speed Up Your Rails Backend Server

blog post publisher

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.
ruby-on-rails
web-development

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.

Rails server log showing dozens of repeated Child Load SELECT queries, one per parent, illustrating the N+1 problem

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:

Rails log after using includes: just one Parent Load and one Child Load query with a single WHERE parent_id IN clause

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 where or order clause, 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.

Frequently asked questions

It happens when your code runs one query to load a set of records, then one extra query per record to load an association. With N records, you get N+1 queries, which adds latency and slows your backend.
Use the includes method to eager-load associations, for example Parent.includes(:children).all. This collapses the N+1 queries into two: one to load the parents and one to load all the children.
includes preloads associated models to avoid N+1 queries. references forces those tables to be joined rather than loaded separately. joins is used when you only need an associated table in a where or order clause and won't render it.
Use the Bullet gem, the most popular Rails option. Enable it in your test environment so it flags N+1 queries when you run tests locally and in CI. You can also inspect a slow controller's query logs by eye.
In our local benchmarks, using includes made queries roughly 2 to 4 times faster. For example, 1,000 parents with 10,000 children dropped from 3.38 seconds to 1.85 seconds.
Victor Motogna

Written by

Victor Motogna

Head of Web Development

Victor Motogna is the Head of Web Development at Wolfpack Digital, leading the web development team and driving innovation in scalable, secure web applications. With a Bachelor's in Computer Science and a Master's in High Performance Computing & Big Data Analytics, he brings deep technical expertise and a forward-thinking approach to building enterprise-grade solutions.


As both a technical leader and hands-on contributor, Victor works across the full technology stack including Ruby on Rails, Vue.js, Nuxt.js, JavaScript, and Python, with extensive experience in DevOps frameworks and cloud infrastructure (Azure, AWS, Kubernetes). His role extends beyond traditional web development—he plays a key part in architecting AI-powered features, training machine learning models, and ensuring AI integration delivers genuine business value rather than following trends.


Victor's leadership philosophy centers on balancing technical excellence with practical delivery. He excels at translating complex technical concepts into clear business language, architecting solutions that strike the right balance between technical sophistication and MVP speed, and staying ahead of rapid technological change. His approach emphasizes building stable, secure end-to-end solutions while constantly seeking smarter, more efficient development processes.


A frequent speaker at technology conferences across Europe, Victor shares insights on modern web development practices, AI integration strategies, cloud architecture, and building high-performing development teams. His writing draws on real-world experience delivering 250+ digital products and reflects his commitment to using technology to create meaningful solutions that improve people's lives.


Through his blog contributions, Victor explores topics at the intersection of web development, AI, and entrepreneurship, focusing on practical implementation strategies, technology decision-making, and fostering knowledge exchange within development teams.


Areas of expertise: Web application architecture, Ruby on Rails development, Vue.js/Nuxt.js, AI integration, machine learning model training, DevOps and cloud infrastructure, team leadership, full-stack development, technical strategy, scalable systems design.

View profile