Blog/
Your API is fine. Your query log is not: the N+1 problem
One list query, then one query per row for related data , that is how a clean-looking ORM loop turns into 101 database round trips. Here is how to spot and fix N+1.

Your API feels slow. The database is fine. The code looks fine. Then you open the query log and see 101 queries for a page that should need 2.
That is the N+1 problem.
You fetch a list once. Say 100 posts. That is the “1.” Then, somewhere in a loop, you ask for related data per item: comments, author, tags. That is the “N.” One query for the list, plus one query per row. With 100 posts, you just paid for 101 round trips to the database for something a join or a batch load could have done in two.
It shows up a lot with ORMs and “clean” looking code. Something like:
posts = await db.posts.findMany()
for (const post of posts) {
post.comments = await db.comments.findMany({ where: { postId: post.id } })
}
Readable. Also brutal at scale. Latency adds up per query, connection pool pressure goes up, and the page gets slower as the list grows. Ten items feel okay. A hundred does not.
How I usually fix it
- Eager load / include. Tell the ORM to fetch the relations in the same trip (Prisma include, Django select_related / prefetch_related, Rails includes, etc.).
- Batch with IN. One query for posts, one for all comments where post_id IN (...), then stitch in memory.
- Join when you only need a flat shape. One SQL statement can return posts plus the related rows you care about.
The rule of thumb: if you are hitting the database inside a loop over a list you just loaded, stop and ask whether that should be one batch instead.
How to catch it before users do
Turn on query logging in local or staging, load a realistic list size, and count. If the count climbs with the list length, you found an N+1.
This is not exotic architecture. It is one of those silent costs that looks like “the app got slower somehow” until you read the log.
Written by Abdul Wahab
← All posts