How N+1 Queries Slow an API and How to Spot Them
An endpoint that returns 20 blog posts with their authors takes 40 milliseconds on your laptop. In production, with 500 posts per page requested by a mobile app, it takes 2.8 seconds and the database CPU graph looks like a mountain range.

An endpoint that returns 20 blog posts with their authors takes 40 milliseconds on your laptop. In production, with 500 posts per page requested by a mobile app, it takes 2.8 seconds and the database CPU graph looks like a mountain range. Nothing in the code looks wrong. There is no slow query in the log, just thousands of fast ones.
That pattern is the N+1 query problem, and it is behind a large share of slow APIs I have been asked to profile. N+1 queries are easy to write, invisible in small test data and painful at scale. The good news is that they are also among the easiest performance problems to find and fix once you know what to look for.
What N+1 Queries Actually Are
The name describes the count. Your code runs one query to fetch a list of N records, then one more query for each record to fetch something related. Fetch 20 orders, then loop over them and load each order's customer, and you have run 21 queries. Fetch 500, and you have run 501.
Each of those queries is fast on its own, often under a millisecond inside the database. The cost is everything around it: a network round trip to the database, connection checkout from the pool, query parsing, result serialisation and object creation in your application. At 1 to 3 milliseconds of overhead per round trip, 500 extra queries adds one to one and a half seconds before any real work happens.
Why ORMs Make N+1 Queries So Easy to Write
Most N+1 problems come from object relational mappers doing exactly what they were designed to do. Lazy loading means a related object is fetched only when your code touches it. Write order.customer.name inside a loop, and the ORM quietly runs a query on every pass.
This is common in Django, Rails ActiveRecord, Laravel Eloquent, Hibernate, Sequelize and Prisma alike. Serialisers make it worse, because a JSON serialiser that includes a nested field walks every relationship for every row, and the query lives in a file nobody thinks of as database code.
GraphQL multiplies it
GraphQL resolvers run per field, per object. A query asking for posts, each post's author and each author's latest three comments can turn one request into hundreds of queries unless the server batches lookups.
How to Spot N+1 Queries Before Users Do
You will not see N+1 problems in a slow query log, because no single query is slow. You need to count queries per request instead.
- Development query logging. Turn on SQL logging locally and watch for the same statement repeated with a different id each time.
- Debug toolbars. Django Debug Toolbar, Laravel Debugbar and similar tools show a query count per page and highlight duplicates.
- Detection libraries. The Bullet gem for Rails and nplusone for Django flag lazy loads in a loop and can fail tests when one appears.
- APM traces. In production, tracing tools show a single request as a waterfall. A long column of identical short database spans is the signature.
The most useful habit is a query count assertion in tests. Write a test that calls the endpoint with ten records and with fifty, and assert the query count is the same. If it grows with the data, you have an N+1.
Three Ways to Fix Them
Every fix comes down to fetching related data in bulk instead of one row at a time.
| Approach | How it works | Best for |
|---|---|---|
| Join in one query | Fetch parents and children with a SQL join | One to one or small one to many relations |
| Eager load with a second query | Fetch all children with WHERE id IN (list) | Larger one to many relations |
| Batch loader | Collect ids during a request, load them together | GraphQL and deeply nested data |
In ORMs these have familiar names: select_related and prefetch_related in Django, includes in Rails, with in Laravel, include in Prisma and Sequelize, JOIN FETCH in Hibernate. For GraphQL, the DataLoader pattern collects every author id requested during one tick and runs a single query for all of them.
The result is usually dramatic. That 501 query endpoint becomes 2 queries, and response time drops from seconds to tens of milliseconds.
The Advice to Always Eager Load Everything
Once a team gets burned by N+1 queries, someone suggests a rule: always eager load every relation, everywhere. That swaps one problem for another.
Eager loading a relation you do not use still costs a query, or a heavier join, and the memory to hold the results. A join across three one to many relations can multiply row counts, so 100 posts with 10 comments and 5 tags each return 5,000 rows to build 100 objects. I have seen endpoints get slower after a blanket eager loading change, not faster.
Load what the response actually uses. That is one more reason to design responses deliberately, with explicit fields and optional includes, as described in how to design a REST API other developers enjoy using. When the shape of a response is fixed, its queries are predictable.
Keep N+1 Queries From Coming Back
N+1 problems return whenever someone adds a field to a serialiser or a template. A fix that relies on everyone remembering will not last. A few guards that do:
- Enable strict lazy loading in development where the ORM supports it. Laravel's preventLazyLoading and Rails strict_loading raise an error on any lazy load.
- Add query count assertions to tests for every list endpoint.
- Set an alert on queries per request in your APM tool, for example anything above 50.
- Review serialiser and resolver changes as database changes in code review.
Fewer queries per request also means each client costs your database less, which matters when you protect busy endpoints with limits, as covered in how rate limiting protects an API without punishing real users.
FAQ
Is a database cache a fix for N+1 queries?
It hides the symptom. Caching may cut database load, but you still pay the round trip to the cache for every row, and cold caches bring the problem straight back. Fix the queries first.
Are N+1 queries ever acceptable?
For lists that are always tiny, such as five settings rows on an admin page, the cost is negligible. The danger is lists that grow with users or content.
Do NoSQL databases have the same problem?
Yes. Fetching a list of documents and then one lookup per document is the same pattern. Embedding related data or using batch reads solves it.
Count Queries, Not Milliseconds
Pick your three busiest list endpoints this week. Turn on query logging, call each with a realistic page size and count the queries. If the count grows with the number of rows, eager load only what the response needs, add a query count test and turn on strict lazy loading in development. Most teams find at least one N+1 on the first try. More backend guides live in our backend section.
More in Back-End
Back-End
How Database Connection Pooling Keeps an API Responsive
Your API answers in 40 milliseconds on a quiet afternoon, then a traffic spike hits and requests start timing out while the...
Back-End
How Idempotency Keys Stop Duplicate API Requests
A customer taps Pay, the spinner hangs for eight seconds on a weak train connection, and they tap again.
Back-End
Why a CORS Error Is a Server Fix, Not a Browser Bug
The red line in the console reads something like "has been blocked by CORS policy", and the front-end developer staring at it...