It’s a familiar story. You build a new feature. It works perfectly on your development machine. You click around and everything is snappy. You ship it. A week later you get a report that the new page is incredibly slow. What happened?
The data on your machine is not the same as the data in production. In development you might have ten users and fifty posts. In production you have ten thousand users and fifty thousand posts. The code that was fast for ten records is painfully slow for ten thousand.
More often than not the culprit is something called an N+1 query. It is the most common performance bug in applications that use an ORM like ActiveRecord. And it’s a silent killer because it feels harmless in development.
An N+1 query happens when your code retrieves a list of objects and then loops through that list to retrieve a related object for each one. For example you fetch 100 posts from the database (1 query). Then you loop through those 100 posts and for each one you fetch its author (100 more queries). You just made 101 database calls to do something that should have only taken two.
Our development environments are designed for speed of creation not for simulating production scale. We use small SQLite databases and a handful of seed records. The latency of a database call on your own laptop is near zero. A hundred near zero queries still feels like zero.
In production those queries go over a network to a database server that is doing other work. Each call has overhead. A hundred small queries with network latency is slow. A thousand is a disaster. The problem isn’t the queries themselves but the sheer number of round trips to the database.
You can’t fix a problem you can’t see. Trying to find N+1 queries by just reading your code is hard. They can hide in view helpers or serializers. You need a tool that watches your application and points them out to you.
Luckily for Rails developers there is a wonderful tool for this called Bullet. Bullet runs in the background of your application in development mode. It monitors the queries your app makes and when it detects an N+1 it tells you. It will pop up an alert in your browser give you a notification or just log to a file.
Getting started is simple. Add it to your Gemfile in the development group.
# Gemfile
group :development do
gem 'bullet'
end
Then run bundle install. After that you need to configure it. The best place is in your config/environments/development.rb file.
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true # browser popup
Bullet.bullet_logger = true # log to a file
Bullet.console = true # log to the browser console
end
Now just use your application. Click around the pages that feel slow in production. If you have an N+1 query Bullet will tell you exactly which line of code is causing it and suggest a fix. This moves you from guessing to knowing.
Once Bullet has identified the problem the solution is almost always to eager load the data. Instead of letting your loop make N extra queries you tell ActiveRecord to fetch all the associated data up front in a single additional query.
You do this with the includes method.
Let’s look at a typical example in a controller. Here is the code that causes an N+1 query.
# app/controllers/posts_controller.rb
def index
@posts = Post.last(100)
end
# app/views/posts/index.html.erb
<% @posts.each do |post| %>
<%= post.title %> by <%= post.author.name %>
<% end %>
Bullet will see this and tell you that you should eager load the :author association on the Post model. The fix is to add .includes(:author).
# app/controllers/posts_controller.rb
def index
@posts = Post.includes(:author).last(100)
end
That’s it. One word. Now instead of 101 queries Rails will run just two. One for the posts and one for all the authors associated with those posts. The result is the same but the performance is dramatically better.
Sometimes you need to load associations of associations. For example what if each post has many comments and each comment has an author? Your view might look like this.
<% @posts.each do |post| %>
<h2><%= post.title %></h2>
<% post.comments.each do |comment| %>
<p><%= comment.body %> by <%= comment.author.name %></p>
<% end %>
<% end %>
This is a recipe for thousands of queries. But the fix is still simple. You just tell includes about the nested relationship using a hash.
# app/controllers/posts_controller.rb
def index
@posts = Post.includes(comments: :author).last(20)
end
With this change ActiveRecord will fetch the posts the comments for those posts and the authors for those comments all in just three database queries. It’s an incredibly powerful tool for managing complexity.
You may also see preload and eager_load mentioned. For most cases includes is the one you want. It’s the smartest of the three. It will look at your query and decide whether to use a separate query (like preload) or a big LEFT JOIN (like eager_load). Don’t worry about the others until you have a specific performance problem that includes can’t solve.
The N+1 problem is a classic example of where our intuition about performance fails us. What looks like simple readable code can hide a performance nightmare.
The solution is to stop guessing and use tools that show you what’s actually happening. Adding Bullet to your development environment is one of the easiest and most effective things you can do to keep your Rails application fast. It takes five minutes to set up and saves hours of frustrating performance debugging. Once you fix your N+1 queries you might find other slow spots. For those you may need a different tool like the one described in A Simple Guide to PostgreSQL EXPLAIN ANALYZE.
But start here. The path to a faster app begins with eliminating these simple mistakes.
— Rishi Banerjee
September 2025