December 5, 2025 5 min read By Farrizal Alchudry Mutaqien

Finding N+1 Queries in Rails

How I usually detect and clean up N+1 queries in Rails without turning the code into SQL soup.

Finding N+1 Queries in Rails

N+1 queries are easy to miss because the page still works.

The first version ships. The table has ten rows. Then production gets real data, and one request starts doing hundreds of small queries. This is the Rails performance issue I check before reaching for more complex fixes.

The shape

This looks harmless:

posts = Post.all

posts.each do |post|
  puts post.author.name
end

But Rails loads author lazily:

SELECT * FROM posts;
SELECT * FROM authors WHERE id = 1;
SELECT * FROM authors WHERE id = 2;
SELECT * FROM authors WHERE id = 3;

One query for the list, then one query per row.

Start with Bullet

In development, I still like bullet because it shows the problem while the page is open:

# Gemfile
gem 'bullet', group: [:development, :test]
# config/environments/development.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.rails_logger = true
  Bullet.console = true
end

When it finds a problem, the message is usually direct:

N+1 Query detected
  Post => [:author]
  Add to your query: .includes(:author)

I still check the Rails log too. Repeated SQL with only the id changing is the giveaway.

Load the relation up front

Most of the time, includes is enough:

posts = Post.includes(:author).where(published: true)

posts.each do |post|
  puts post.author.name
end

Rails now loads posts and authors in batches:

SELECT * FROM posts WHERE published = true;
SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5);

Nested relationships work the same way:

Post.includes(author: :profile, comments: :user)

When includes is not what I want

I keep these three in mind:

MethodWhat it doesWhen I use it
includesLets Rails chooseDefault option
preloadSeparate queriesI do not need SQL conditions on the join
eager_loadLEFT OUTER JOINI need to filter or order by the association

Example:

Post.eager_load(:author).where(authors: { active: true })

Places I check first

The N+1 often hides outside the controller.

Serializers:

class PostSerializer
  def author_name
    object.author.name
  end
end

Views:

<% @posts.each do |post| %>
  <%= post.author.avatar_url %>
<% end %>

Counts:

post.comments.count

For counts, I usually add a counter cache:

class Comment < ApplicationRecord
  belongs_to :post, counter_cache: true
end

Then read:

post.comments_count

A stricter option

For newer Rails apps, strict loading is useful in development:

config.active_record.strict_loading_by_default = true

Or per query:

Post.strict_loading.includes(:author)

Lazy loading then raises instead of quietly adding another query.

How I check it

When a Rails page feels slow, I check this order:

  1. Open the page with Bullet enabled.
  2. Scan the Rails log for repeated queries.
  3. Add includes, preload, or eager_load.
  4. Check serializers and views.
  5. Use counter caches for repeated counts.

Most slow Rails pages get easier to reason about after this pass.

Back to blog