← All posts

July 4, 2026

Catch N+1 queries early with the Bullet gem

An N+1 query is the easiest performance bug to write and the hardest to notice. The page loads fine on your machine with three records. Then a real account shows up with three hundred, and the same page fires three hundred queries.

You don't see it because nothing is broken. It's just slow, and slow creeps in quietly.

Bullet makes it loud. It watches your queries as you click around and tells you the moment a page loads a collection and then queries each record one by one. It also flags the opposite mistake: eager loading you added and never used.

I wired it into this app in about five minutes. Here's the setup.

Install

One gem, in the development and test groups only. You never want this running in production.

group :development, :test do
  gem 'bullet' # Flags N+1 queries and unused eager loading
end
bundle install

Configure development

In config/environments/development.rb, turn it on and pick how you want to be told. I use two channels: a small pill in the page footer, and a JavaScript alert that pops the second an offense happens. No logger, because I want the warning in my face, not buried in the terminal.

# Bullet: surface N+1 queries and needless eager loading while developing.
# Show the on-page pill (add_footer) and a JS alert on offenses; skip the logger.
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.add_footer = true
end

Now load a page with an N+1 and Bullet tells you exactly what it saw and how to fix it:

Bullet flagging an N+1 query in the app footer

That's a real one from this app. I had dropped the .includes on the projects page, and Bullet caught it on the first load. Notice it doesn't just point at the problem. It hands you the fix:

# Before: one query for the projects, then one per project for its memberships
@projects = current_user.projects.alphabetical

# After: two queries total
@projects = current_user.projects.alphabetical.includes(memberships: :user)

Configure test

Development warnings only work if you're looking. The stronger move is to make an N+1 fail the build, so a regression can't merge.

In config/environments/test.rb, enable Bullet and tell it to raise:

# Bullet: catch N+1 queries during the test suite too. Raise on offenses so a
# test that triggers an N+1 fails loudly; no logger.
config.after_initialize do
  Bullet.enable = true
  Bullet.raise = true
end

Any integration or system test that exercises a page with an N+1 now throws instead of passing. The bug stops being something you might notice and becomes something CI won't let through.

Why this is worth five minutes

N+1s don't announce themselves. They pass code review, they pass the demo, they pass every test that runs against a handful of rows. They only show up in production, on your biggest account, on the page they care about most.

Bullet moves that discovery to the exact moment you write the code, when the fix is one line and you still have the whole context in your head. That's the cheapest possible time to catch it.