Your page feels slow and the log shows the same query repeating? Those are rails n+1 queries: one query loads a list, then one more fires per row. Find them with Bullet and fix them with eager loading.
This page is one part of the wider topic of ruby on rails debugging, where slow pages and odd errors are covered side by side.
| Item | Value |
|---|---|
| Queries for 100 posts with an N+1 pattern | 101 queries |
| Queries for the same page with eager loading | 2 queries |
| Database round trips saved | 99 fewer round trips |
| Main eager-loading methods in Rails | 3 (includes, preload, eager_load) |
| Goldiloader support | Rails 7.2+ and Ruby 3.2+ |
The figures come from the OneUptime guide and the Goldiloader documentation. They can change, so check the source before you rely on them.
What an N+1 query looks like in Rails
An N+1 query pattern means your code runs one query to load a collection, then one extra query per record to load an association. As the OneUptime guide puts it, the N is the number of records and the +1 is the initial query.
Picture a page that lists 100 posts with each author’s name. The N+1 version needs 101 queries, while eager loading needs 2, which removes 99 round trips to the database, according to the same guide.
Rails makes this easy to trigger because of lazy loading. Scout’s guide notes that every belongs_to, has_many and has_one association is a potential N+1 waiting to happen.
Each extra query pays for parsing, planning and network latency, so a loop of tiny queries adds up fast. If a loop over associations ends in a nil error instead of a slowdown, see undefined method for nil rails.
How to spot N+1 queries before users do
You have three practical places to look, from cheapest to most thorough:
- The development log, where the same SELECT repeats with a different id.
- The Bullet gem, which flags the pattern while you click through the app or run tests.
- A production monitor, which sees real traffic and real data volumes.
In the log, an N+1 shows up as a run of near-identical statements, such as SELECT * FROM authors WHERE id = 1, then id = 2, then id = 3, repeated for every post, as shown in Scout’s example. The OneUptime guide also suggests turning on detailed query logging, for example with the verbose_query_logs setting, to spot these patterns.
Bullet is described as the most popular gem for detecting N+1 queries in Rails. Add it to the development and test groups of your Gemfile, then choose how it reports:
- Browser alerts with Bullet.alert.
- Its own log file with Bullet.bullet_logger.
- The Rails logger with Bullet.rails_logger.
- A footer on each page with Bullet.add_footer.
Because Bullet can write into the Rails logger, knowing your rails logger levels helps you decide how much of that output to keep. Bullet also detects unused eager loading, which tells you when an include is loading data you never touch.
Development tools only catch what your tests and clicks exercise. Monitoring products such as Scout flag repeated query patterns in production, where code paths and data sizes differ from your laptop.
Fixing N+1 with includes, preload and eager_load
Rails offers three main eager-loading methods, and all three remove the per-record queries. They differ in the SQL they generate.
| Method | SQL strategy |
|---|---|
| includes | Rails chooses between separate queries and a JOIN |
| preload | Always separate queries, never a JOIN |
| eager_load | Always a LEFT OUTER JOIN |
includes is a sensible default. Writing Post.includes(:author) cuts the query count from N+1 to 2, and if you add a where clause that references the association, Rails switches to a JOIN on its own, per Scout’s guide.
When you filter with SQL strings instead of hashes, Rails cannot see which table you mean. A DEV Community walkthrough says you can use references to trigger the join in that case.

preload always runs a separate query per table. Scout recommends it for many-to-many associations and cases where a JOIN would produce duplicate rows. The same DEV Community walkthrough adds that it tends to be faster than a JOIN even with the extra query overhead, but it cannot filter on the associated table, because the WHERE clause on the first query cannot refer to those columns.
eager_load always uses a LEFT OUTER JOIN, which is the right pick when you need to filter or order by the associated table’s columns. The DEV walkthrough warns that these join queries can be slower than the preload alternative, by an order of magnitude or more in some cases.
All three accept nested associations, so you can load posts, their authors and each author’s publisher in one call with a nested hash such as includes(author: :publisher). Do not confuse any of this with joins: with joins, calling user.address.city in a loop ignores the in-memory values and fires as many new queries as there are users.
Where N+1 hides: jobs, serializers and counts
Controllers and views are the obvious suspects, but the pattern is not limited to them. Three spots catch people out:
- Background jobs. Scout notes the pattern commonly appears in Sidekiq workers, Resque jobs and other processors, where browser tools will not show it. Looping over User.active and reading user.posts is fixed by adding includes(:posts) to the first query.
- Serializers. With Active Model Serializers, JBuilder or Blueprinter, the serialization layer can trigger lazy loads. Fix it in the controller by eager loading before you render.
- Counts. Calling post.comments.count for every post fires one COUNT query per row.
For the count case, use a counter cache. Add a comments_count integer column to posts with a default of 0, then declare counter_cache: true on the belongs_to in Comment. After that, post.comments.size reads the cached column instead of issuing a COUNT query.
Rails’ own tricks do not rescue you here. Query results are cached and relations are lazy, but the DEV Community walkthrough points out that neither helps when loops in application code fire multiple unique queries.
Keeping N+1 queries out of production code
Fixing one page is easy; keeping it fixed is the hard part. A short routine works well:
- Reproduce the slow page in development with Bullet enabled.
- Read the report or the log to find the association being loaded one row at a time.
- Add includes, preload or eager_load for that association at the query that loads the list.
- Reload the page and confirm the repeated SELECT lines are gone.
- Add a test that exercises the page so a regression fails loudly.
For step five, set Bullet to raise in the test environment so CI fails on N+1 patterns. Scout adds that Bullet supports an allowlist, so you can enforce the rule for new code while you clean up existing issues over time.
If Bullet misses something, Prosopite is a newer alternative that works at the SQL level rather than the ORM level, so it can catch patterns Bullet might miss, including raw SQL.
Some teams prefer automation. Goldiloader is an ActiveRecord extension that eager loads associations automatically, assuming that if you access an association on one record in a collection you will do so for all of them. As of its documentation indexed on Aug 1, 2026, it supports Rails 7.2+ and Ruby 3.2+, and it warns against automatic eager loading for has_one associations that rely on a LIMIT, as listed in the Goldiloader docs.
Frequently asked questions
What is the difference between includes and eager_load?
Show answer
includes lets Rails pick separate queries or a JOIN, while eager_load always uses a LEFT OUTER JOIN. Use eager_load when you must filter or order by the associated table.
Does Bullet catch every N+1 query?
Show answer
No. It only sees code your tests and browsing actually run, so production-only paths can slip through. A SQL-level tool such as Prosopite or a production monitor fills part of that gap.
Can N+1 queries happen outside controllers?
Show answer
Yes. Background jobs and serializers are common sources, and they are often harder to notice because they do not show up in browser-based tools.
Should I eager load everything to be safe?
Show answer
No. Loading associations you never use wastes work, and Bullet can report unused eager loading so you can remove those includes.



