Skip to content
Performance Optimization·

The N+1 Query Problem: The Silent Performance Killer in Almost Every Web App

By Bahaj Abderrazak·Published October 23, 2026·Updated October 23, 2026

Your page makes 3 database queries in development and 3,000 in production. That's the N+1 problem — the most common performance bug I find in Laravel, Prisma and Rails apps, how to spot it in minutes, and how to fix it for good.

Code on a screen representing ORM queries in a web application

There's a performance bug so common that I check for it in the first ten minutes of every database audit, and so quiet that applications ship with it for years. It's called the N+1 query problem, and if your app uses an ORM — Laravel's Eloquent, Prisma, Django, Rails, it doesn't matter — the odds are decent that you have one somewhere right now, quietly waiting for your data to grow.

What N+1 actually means

You load a list of N things, then, for each thing, you fire one more query to fetch something related. 100 orders on a page, then 100 separate queries for each order's customer. Total: 101 queries where 2 would do. Display 1,000 rows and it's 1,001 queries.

The code that causes it looks completely innocent. In Laravel:

$orders = Order::all();
foreach ($orders as $order) {
    echo $order->customer->name;  // one query per order, hidden in a property
}

In Prisma, it's the loop that awaits a relation per item instead of using include. The ORM makes the extra queries invisible — no line of code says "query" — which is precisely why the bug survives code reviews. And it scales in the worst possible direction: the busier and more successful your page gets, the more queries it fires.

Why it hurts more than the raw count suggests: each query is a network round trip plus parsing plus planning. One 0.5ms query is nothing; 2,000 of them in sequence is a full second of pure overhead before the database does any real work — and under concurrent users, that many small queries also churn through connections and CPU, slowing everyone's requests, not just yours. I see it constantly behind the complaint "the site is fine for me but dies at peak hours."

How to spot it (in minutes, honestly)

  • Count queries per page. Laravel Telescope or Debugbar, Prisma's query logging, Django Debug Toolbar — any of them will show you the number. A list page making hundreds of queries has an N+1 until proven otherwise.

  • Look for the pattern in the log. Dozens of near-identical queries differing only in an ID (SELECT * FROM customers WHERE id = ? × 300) is the fingerprint. One query for the list + repeats for the details = N+1.

  • Check where it doesn't hurt yet. Development data is small, so N+1 hides. Sort your pages by "queries × rows displayed" and check the worst offenders against realistic data volumes, not your 12 test records.

The fixes, by situation

Eager loading (the 90% fix). Tell the ORM upfront what you'll need: Order::with('customer') in Eloquent, include: { customer: true } in Prisma, select_related/prefetch_related in Django. The ORM then fetches the relations in one extra query with an IN (...) list, and your 1,001 queries become 2.

A JOIN, when the relation is always needed and filtered. If you're filtering or ordering by the related data ("orders whose customer is in Morocco"), eager loading won't help — that's a database join's job, or a whereHas in Eloquent terms.

Select only the columns you use. Half the "N+1s" I find ride along with SELECT * on wide tables. Fetching three columns instead of forty shrinks every one of those queries and the memory they chew.

Batch and cache the genuinely hot paths. A dashboard header that queries the same reference data for every user doesn't need cleverness — it needs a cache. (Knowing when caching is the right answer vs. a bandage is its own topic — see my Redis caching guide.)

Aggregate in SQL, not in PHP/JS. Counting, summing and averaging in application loops after fetching rows is N+1's cousin. COUNT, SUM and GROUP BY exist so the database — which is right there next to the data — does the arithmetic once.

The prevention habit

Fixes rot without a habit behind them. Three that cost nothing:

  1. Query budgets in review. "This page should run in under 15 queries" is a rule a reviewer can actually check with the debug toolbar open.

  2. Seed realistic data. A dev database with 10,000 orders instead of 10 makes N+1s visible while they're still free to fix.

  3. Watch the slow-query log in production. Recurring near-identical queries at scale announce themselves there first — the same monitoring discipline from my slow database guide.

The N+1 problem is, in a way, good news: it's a bug with a known shape, cheap detection and usually a one-line fix per instance — and removing a bad one can take a page from three seconds to 200 milliseconds without touching a server or an index. If your busiest pages feel heavy and you don't know how many queries they run, that's your first measurement to take. And if you'd like an experienced eye on the log, that's my day job — the first findings are usually embarrassingly fixable.

N+1 problem fixeager loadingORM performancetoo many database queriesN+1 query problem

Let's begin

Have an idea worth building?

Tell me what you are creating, what stage the project is at, and where you need technical support — I will respond with practical next steps.