N+1 queries are the single most common performance problem in web applications that use an ORM. They happen when your code fetches a list of N records, then executes a separate database query for each record to load related data. Instead of 1 or 2 queries, you end up with N+1.
Many major ORMs are susceptible: ActiveRecord in Rails, Django’s ORM, and Ecto in Elixir. The pattern looks slightly different in each framework, but the cause and the fix follow the same logic. If you work across multiple frameworks (or your team does), understanding the pattern once means you can spot it anywhere.
In this guide:
- What N+1 queries look like and why they hurt performance
- How to detect and fix them in Rails, Django, and Elixir
- Tools for catching them before they hit production
- How Scout automatically identifies N+1 patterns without configuration
For deeper framework-specific coverage, see our dedicated guides: Rails N+1 Queries, Django N+1 Queries, and Elixir/Ecto N+1 Queries.
What Is an N+1 Query?
The pattern is straightforward. You query a list of records (1 query), then loop through them and access a related record on each one (N queries). The total is N+1 queries where 1 or 2 would have been enough.
Here is a simple example in Rails:
# 1 query to fetch all posts
posts = Post.all
# N queries, one per post, to fetch each author
posts.each do |post|
puts "#{post.title} by #{post.author.name}"
end
The same pattern in Django:
# 1 query to fetch all books
books = Book.objects.order_by("title")
# N queries, one per book, to fetch each author
for book in books:
print(book.title, "by", book.author.name)
And in Elixir with Ecto:
# 1 query to fetch all posts
posts = Repo.all(Post)
# N queries, one per post, to fetch each author
Enum.each(posts, fn post ->
author = Repo.preload(post, :author).author
IO.puts("#{post.title} by #{author.name}")
end)
In all three cases, the database executes 1 query to load the list, then N separate queries to load each related record. With 10 records, that is 11 queries. With 1,000 records, that is 1,001 queries. Each query carries network and execution overhead, and the total response time grows linearly with the number of records.
Why N+1 Queries Matter
| Records | Queries Without Fix | Queries With Fix | Typical Impact |
|---|---|---|---|
| 10 | 11 | 1-2 | Barely noticeable |
| 100 | 101 | 1-2 | Noticeably slower |
| 1,000 | 1,001 | 1-2 | Seconds of delay |
| 10,000 | 10,001 | 1-2 | Timeout or crash |
The problem scales linearly, which means it often goes unnoticed in development (where you have 5 records) and becomes a crisis in production (where you have 5,000). This is why automated detection matters more than code review for catching N+1 patterns.
How to Fix N+1 Queries
The fix is the same concept across all frameworks: tell the ORM to load related data upfront instead of lazily. The terminology differs, but the idea is identical.
Rails: includes, preload, and eager_load
# Fix: eager load authors in one query
posts = Post.includes(:author)
posts.each do |post|
puts "#{post.title} by #{post.author.name}"
end
Rails provides three methods:
| Method | Strategy | Best For |
|---|---|---|
includes |
Lets Rails choose JOIN or separate query | Default choice |
preload |
Always uses a separate query | Many-to-many, avoiding duplicate data |
eager_load |
Always uses a LEFT OUTER JOIN | When you need to filter on the association |
For a deep dive into Rails-specific patterns including Sidekiq jobs, ActiveRecord callbacks, and the Bullet gem, see our Rails N+1 Query Guide.
Django: select_related and prefetch_related
# Fix: eager load authors with a JOIN
books = Book.objects.order_by("title").select_related("author")
for book in books:
print(book.title, "by", book.author.name)
| Method | Strategy | Best For |
|---|---|---|
select_related |
Uses a SQL JOIN | ForeignKey and OneToOneField |
prefetch_related |
Uses a separate query | ManyToManyField and reverse ForeignKey |
For the complete Django treatment including django-auto-prefetch, nested queries (2N+1, NM+N+1), and detection with django-debug-toolbar, see our Django N+1 Queries Guide.
Elixir/Ecto: Repo.preload and Ecto.Query.preload
# Fix: preload authors in a separate query
posts =
Post
|> Repo.all()
|> Repo.preload(:author)
Enum.each(posts, fn post ->
IO.puts("#{post.title} by #{post.author.name}")
end)
Or inline with the query:
posts = from(p in Post, preload: [:author]) |> Repo.all()
| Method | Strategy | Best For |
|---|---|---|
Repo.preload/2 |
Separate query after fetch | Simple cases, pipeline style |
from ... preload |
Inline with query | When building complex queries |
join + preload |
Uses a JOIN | When filtering on the association |
For Ecto-specific patterns including Oban jobs, Phoenix LiveView, and nested preloads, see our Elixir/Ecto N+1 Queries Guide.
Tools for Detecting N+1 Queries
Fixing N+1 queries is easy once you find them. Finding them is the hard part. Here is a comparison of the most common detection approaches across frameworks.
| Tool | Framework | Detects Automatically | Works in Production | Shows Code Location |
|---|---|---|---|---|
| Scout | Rails, Django, Elixir | Yes | Yes | Yes |
| Bullet gem | Rails | Yes | No (dev/test only) | Partial |
| django-debug-toolbar | Django | Manual inspection | No | Yes |
| nplusone | Django | Yes (logging) | Yes | No |
| Ecto telemetry | Elixir | Manual setup | Yes | Partial |
How Scout Detects N+1 Queries
Scout’s approach is different from framework-specific tools because it works at the APM level. The agent monitors every database query inside every request, background job, and code path. When it sees repeated query patterns with varying parameters, it flags the N+1 automatically and shows:
- The exact code location causing the repeated queries
- How many times the query was executed
- The total time impact on the request
- The generated SQL
This works without any configuration or code changes. Install the agent, deploy, and N+1 patterns start appearing in your dashboard. It works equally well in development and production, so you catch problems regardless of where they surface.
For Rails teams, this means you see N+1 patterns in controller actions, Sidekiq jobs, and Action Cable handlers. For Django teams, it covers views, Celery tasks, and management commands. For Elixir teams, it covers Phoenix controllers, LiveView, and Oban workers.
When N+1 Queries Are (Maybe) Acceptable
Not every N+1 query needs fixing. A few cases where the cost of fixing may not be worth it:
- N is always small and bounded. If a page always shows exactly 3 items and never more, the 4 queries may be fast enough.
- The query is already cached. If the related records are in a query cache or application cache, the “extra” queries may not hit the database at all.
- The fix adds complexity that obscures the code. Sometimes an explicit N+1 with a comment explaining why is clearer than a complex prefetch chain.
That said, these are edge cases. In most production applications, N+1 queries should be found and fixed, because the N tends to grow over time even when you think it will not.
Frequently Asked Questions
What is an N+1 query?
An N+1 query is a database performance pattern where your code executes 1 query to fetch a list of N records, then N additional queries to load related data for each record. The fix is to tell the ORM to load related data upfront using eager loading (includes in Rails, select_related/prefetch_related in Django, Repo.preload in Ecto).
How do I find N+1 queries in my application?
The fastest way is to use an APM tool like Scout that automatically detects N+1 patterns in both development and production. Scout shows the exact code location, query count, and time impact without any configuration. Framework-specific tools like the Bullet gem (Rails) or django-debug-toolbar (Django) can also help but are typically limited to development.
Do N+1 queries only happen with ORMs?
No. N+1 patterns can occur anywhere you loop over results and make a query per result, including raw SQL, GraphQL resolvers, REST API calls, and even file system operations. ORMs make N+1 queries especially common because they make lazy loading the default behavior.
Should I fix every N+1 query?
Not necessarily. If N is always small, bounded, and the query is fast, the overhead may be negligible. Focus on N+1 queries that appear in high-traffic code paths or where N can grow unbounded. Scout’s N+1 insights help you prioritize by showing frequency and total time impact.
Does Scout detect N+1 queries automatically?
Yes. Scout monitors every database query in every request and background job, identifies repeated query patterns, and flags N+1 issues with the exact code location and time impact. This works without configuration in Rails, Django, and Elixir applications. See how it works or start a free trial with no credit card required. Our free tier is always available.
Next Steps
If you are working in a specific framework, our dedicated guides go deeper:
- Rails: N+1 Queries in Rails: A Complete Guide covers ActiveRecord
includesvspreloadvseager_load, the Bullet gem, Sidekiq jobs, and CI/CD enforcement. - Django: Django and the N+1 Queries Problem covers
select_related,prefetch_related,django-auto-prefetch, and nested query patterns (2N+1, NM+N+1). - Elixir: Ecto N+1 Queries: Detection and Prevention covers
Repo.preload, inline preloads, Oban jobs, and Phoenix LiveView patterns.
For application monitoring with errors, logs, and traces, Scout Monitoring provides the fastest insights without the bloat. See what we offer for Rails teams, compare Ruby monitoring tools, or start a free trial with no credit card required.