What is the N+1 Query Problem?

Last updated: July 2026

The N+1 query problem is a pattern where fetching N records triggers one extra query per record — N+1 round trips instead of one or two. It is the most common serious performance bug in data-backed applications, and the most camouflaged: every individual query is fast, the code reads perfectly, and the page works — right up until real data sizes arrive.

Key takeaways

QuestionAnswer
The shape1 query for the list + N queries for relations = N+1 round trips
The causeLazy loading touched inside a loop — invisible queries per iteration
The signatureMany fast identical queries — slow-query logs never see it
The exitsEager loading · joins · batched IN · request-scoped loaders
The multiplierRound-trip latency × page size — brutal over networks

The bug, in the open

// 1 query: fetch 100 posts
const posts = await postRepo.findRecent(100);

for (const post of posts) {
  // +1 query PER POST — post.author looks like a property,
  // but lazy loading runs: SELECT * FROM users WHERE id = ?
  render(post.title, (await post.author).name);
}
// Query log: 1 + 100 = 101 round trips for one page render

The fix, as SDKs express it — declare the relation up front, and the platform fetches it in the same request:

// JavaScript / Node.js — Back4app JS SDK
// The fix: fetch the relation in the same request — 1 query, not N+1
const query = new Parse.Query('Comment');
query.equalTo('post', post);
query.include('author');                    // eager-load the pointer
const comments = await query.find();        // one round trip, total

comments.forEach((c) =>
  render(c.get('text'), c.get('author').get('username')) // already loaded
);

The cost, multiplied out

The arithmetic the explainers skip — total time ≈ list query + (N × round-trip latency):

Page size N@1 ms/query@5 ms/queryBatched (1–2 queries)
10~11 ms~55 ms~10 ms
100~101 ms~505 ms~12 ms
1,000~1.0 s~5.0 s~20 ms

Published fixes match the math: pages dropping from 1.4 s to 0.16 s, endpoints speeding up 30× and more. Two corollaries worth pinning: pagination caps N — a bounded page size bounds the blast radius even before the real fix; and the network version is worse — when each of the N calls is an HTTP request rather than a local query, multiply by tens of milliseconds instead of single digits.

The four exits

N plus one waterfall versus batched fetchingThe N+1 pattern issues one list query followed by one query per record in sequence; the batched pattern issues the list query and a single query fetching all related records at once.

the fix

Batched

List query

ONE query for all relations
WHERE id IN (…)

N+1 waterfall

List query

Query per record
× N, one after another

The N+1 pattern issues one list query followed by one query per record in sequence; the batched pattern issues the list query and a single query fetching all related records at once.
FixHowReach for it whenWatch for
Eager loading / includeDeclare relations on the queryThe screen always needs the relationOver-including bloats payloads
JoinOne statement fetches bothRelational engine, report-shaped readsRow explosion on wide joins
Batched INCollect keys, fetch onceAny stack, even hand-rolledOne extra round trip (fine)
Request-scoped loaderAuto-batch during executionGraphQL resolvers, layered codeMust be per-request, not global

The last row is GraphQL’s famous case: resolvers fire per parent object, recreating the loop server-side, and the loader pattern — collect keys in one tick, fetch once, memoize per request — is the industry-standard cure. A subtle rule rides along: loaders are request-scoped; a global one becomes a stale cache with authorization bugs.

Detection: hunt the fast queries

The N+1 signature is inverted from normal performance work: you are looking for many fast identical queries, not one slow one — which is why slow-query logs, the usual tool, never see it. The methods that do: ORM debug logging (the same statement, N different parameters, one render); APM span views, where the waterfall of identical short spans is unmistakable; and the cheapest of all — count the queries for one page load in development, with a threshold in your head: a list page should cost queries in the single digits, not in multiples of its rows. The write-side twin deserves its audit too: an insert-per-item loop is N+1 for writes, fixed with bulk operations.

Common use cases

  • List views with authors, owners, or statuses — the canonical home: every feed, inbox, and table joining people to items.
  • GraphQL APIs — nested list fields are structurally N+1 until loaders exist.
  • Document databases — one find per referenced document; fixed with include, batched IN, or embedding, per the modeling rules.
  • Microservice fan-outs — a list from service A, one HTTP call to service B per item; compound endpoints and BFFs exist to end this.
  • Background jobs — the loop that processes 10,000 records with two queries each, quietly costing hours.

Lazy vs. eager loading: a decision matrix

Load eagerly when…Stay lazy when…
The relation renders on every rowThe relation is behind a click
The loop is the access patternAccess is one-record-at-a-time
N is page-sized or biggerN is guaranteed tiny
Latency is user-facingA background task can afford drift
You just fixed this bug hereYou have measured, not assumed

And the standing audit rule that outlives any matrix: any relation touched inside a loop is guilty until the query log proves otherwise.

Limitations and trade-offs

  • Eager loading can overcorrect. Including heavy relations everywhere trades N+1 for bloated payloads and wide joins — include what the screen renders, not the whole graph.
  • Joins have their own cliff. One-to-many joins duplicate parent rows per child; at high fan-out the two-query batched fetch beats the single join.
  • Loaders add machinery. Request scoping, cache invalidation within the request, and batching windows are real code — the price of automatic batching.
  • Frameworks re-introduce it silently. Serializers, template helpers, and “just one more field” reviews are how fixed pages regress; the query-count check belongs in CI, not memory.
  • The fix is per-path, not global. N+1 is an access-pattern bug; every new screen re-asks the question.

N+1 on Back4app

Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. Its SDKs make the fix the idiom rather than the remediation: relations are typed Pointers, and include() — the code tabs above — fetches them in the same request, batched server-side. The GraphQL API resolves nested queries without per-field fan-out, and pagination defaults bound N before it grows teeth. The loop that causes N+1 has no natural way to be written — which is the best kind of fix: the one nobody has to remember.

Frequently asked questions

What is the N+1 query problem in simple terms?

You fetch a list of N records with one query, then your code makes one more query per record for its related data — a hundred posts become a hundred and one queries. Each query is individually fast, which is exactly why the problem hides: nothing is slow enough to alarm anyone until the page makes hundreds of round trips.

What causes N+1 queries?

Lazy loading as a default. ORMs and SDKs let you navigate relations like object properties — post.author — and transparently run a query when you touch one. Put that access inside a loop over N results and you have silently written N queries. The abstraction that made data access pleasant also made the round trips invisible.

How do you fix the N+1 problem?

Four standard exits, best chosen per case: eager loading — tell the query up front to include the relation; a join that fetches both in one statement; batching — collect the N foreign keys and fetch them in one contained-in query; and request-scoped loaders that batch automatically. All four turn N+1 round trips into one or two.

How much slower is N+1 really?

Multiply it out: each round trip costs one to five milliseconds before the query even runs, so 100 rows at 5 ms adds half a second versus one ~10 ms batched query. Published real-world fixes report pages going from about 1.4 seconds to 0.16, and API endpoints speeding up thirtyfold. The math worsens linearly with page size — and catastrophically over networks.

Why is the N+1 problem so common in GraphQL?

Because resolvers run per field, per object. A query for posts with their authors runs the posts resolver once and the author resolver N times — the ORM loop reborn server-side. The canonical cure is the loader pattern: a request-scoped batcher that collects the author IDs during execution and issues one batched fetch, memoized for the request.

How do you detect N+1 queries?

Look for many fast identical queries, not one slow one — that is the signature. ORM debug logging shows the repeated statement with different parameters; slow-query logs miss it entirely because each query is quick; APM tools flag the span pattern explicitly. The habit that catches it early: read the query log for one page render, and count.

Does the N+1 problem happen in document databases and REST APIs?

Everywhere data has relations. In document stores it is one find per referenced document — fixed by include mechanisms, batched contained-in queries, or embedding. Over REST it is a list endpoint plus one HTTP call per item — worse than the database version because network latency dwarfs query latency; compound endpoints, batch endpoints, and query languages exist largely to kill it.

Is lazy loading always wrong?

No — it is wrong in loops. Lazy loading is exactly right when related data is rarely needed: pay for it on the one record that needs it rather than eagerly for all N. The discipline is knowing which access pattern each screen has: always-needed relations load eagerly, rarely-needed ones lazily, and anything inside a loop gets audited.

Related terms

Compare with

Further reading

Ready to build your backend?

Start your project on Back4app in minutes — database, auth, APIs, and Cloud Code included. No credit card required.

Written and reviewed by Back4app Engineering, Back4app Engineering · Published 2026-07-27