100 items, 101 queries: the N+1 pattern and how to kill it.
N+1 shows up every time you put an `await` inside a `for`. How to spot it in your logs and swap it for a single query.
An endpoint lists 100 stores and takes 3 seconds. The database log shows 101 queries per request: one for the list of stores, one hundred more to count reviews. The user feels it as "slow today." The database feels it as CPU at 80%.
The pattern is old and called N+1. It shows up every time there's an await inside a for or forEach: one initial query fetches N items, and each item triggers one more query to "enrich it."
// ❌ N+1: 101 queries for 100 stores
const stores = await prisma.store.findMany()
for (const store of stores) {
const reviews = await prisma.review.findMany({
where: { storeId: store.id },
})
store.reviewCount = reviews.length
}
// ✅ One query, the ORM does the join + aggregation
const stores = await prisma.store.findMany({
include: {
_count: { select: { reviews: true } },
},
})
Three seconds become 80 milliseconds. More importantly: the cost stops scaling with the data. 100 stores or 10,000, one single query.
N+1's cousin: sequential queries that could be parallel
The other pattern that looks similar without being the same: three awaits in a row that don't depend on each other.
// ❌ Sequential, total time = sum
const store = await getStore(id) // 200ms
const products = await getProducts(id) // 300ms
const analytics = await getAnalytics(id) // 500ms
// Total: 1000ms
// ✅ Parallel, total time = the slowest one
const [store, products, analytics] = await Promise.all([
getStore(id),
getProducts(id),
getAnalytics(id),
])
// Total: 500ms
Before optimizing CPU, kill network round-trips. Network latency is almost always the real bottleneck. One Promise.all can save more time than a week of micro-optimization.
How to spot it without reading code
Run the request and count the queries in the database log. If the query count grows with data volume, you have an N+1. If it's constant, you're fine.
It's the simplest performance audit for an endpoint you didn't write yourself.