---
term: 'N+1 Query Problem'
seoTitle: 'The N+1 Query Problem: Causes, Detection & Fixes'
headline: 'What is the N+1 Query Problem?'
slug: n-plus-one-query-problem
category: api-realtime
shortDefinition: '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.'
relatedTerms:
  - relational-queries-document-databases
  - graphql-vs-rest
  - overfetching-underfetching
  - api-payload-optimization
  - database-index
contrastsWith:
  - overfetching-underfetching
faq:
  - question: 'What is the N+1 query problem in simple terms?'
    answer: '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.'
  - question: 'What causes N+1 queries?'
    answer: '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.'
  - question: 'How do you fix the N+1 problem?'
    answer: '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.'
  - question: 'How much slower is N+1 really?'
    answer: '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.'
  - question: 'Why is the N+1 problem so common in GraphQL?'
    answer: '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.'
  - question: 'How do you detect N+1 queries?'
    answer: '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.'
  - question: 'Does the N+1 problem happen in document databases and REST APIs?'
    answer: '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.'
  - question: 'Is lazy loading always wrong?'
    answer: '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.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'DataLoader pattern — graphql-js documentation'
    url: 'https://www.graphql-js.org/docs/n1-dataloader/'
  - name: 'Solving the N+1 problem for GraphQL through batching — Shopify Engineering'
    url: 'https://shopify.engineering/solving-the-n-1-problem-for-graphql-through-batching'
  - name: 'N+1 queries — Sentry performance issue documentation'
    url: 'https://docs.sentry.io/product/issues/issue-details/performance-issues/n-one-queries/'
  - name: 'SDK query documentation (include)'
    url: 'https://docs.parseplatform.org/js/guide/#relational-data'
cta:
  title: 'One request where others make a hundred'
  text: 'Back4app SDKs make the fix the default idiom: include() fetches relations in the same request, batched server-side, with pointers keeping the joins cheap. The N+1 loop simply has no reason to exist in your codebase.'
  linkText: 'Start building free'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-07-27'
translationKey: n-plus-one-query-problem
---

**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

| Question | Answer |
| --- | --- |
| The shape | 1 query for the list + N queries for relations = N+1 round trips |
| The cause | Lazy loading touched inside a loop — invisible queries per iteration |
| The signature | Many *fast identical* queries — slow-query logs never see it |
| The exits | Eager loading · joins · batched IN · request-scoped loaders |
| The multiplier | Round-trip latency × page size — brutal over networks |

## The bug, in the open

```javascript
// 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:**

```javascript
// 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
);
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// The fix: fetch the relation in the same request — 1 query, not N+1
final query = QueryBuilder<ParseObject>(ParseObject('Comment'))
  ..whereEqualTo('post', post.toPointer())
  ..includeObject(['author']);              // eager-load the pointer
final response = await query.query();       // one round trip, total
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// The fix: fetch the relation in the same request — 1 query, not N+1
let query = Comment.query("post" == post)
  .include("author")                        // eager-load the pointer
query.find { result in
  if case .success(let comments) = result { // one round trip, total
    comments.forEach { render($0.text, $0.author?.username) }
  }
}
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// The fix: fetch the relation in the same request — 1 query, not N+1
val query = ParseQuery.getQuery<ParseObject>("Comment")
query.whereEqualTo("post", post)
query.include("author")                     // eager-load the pointer
query.findInBackground { comments, e ->     // one round trip, total
  if (e == null) comments.forEach {
    render(it.getString("text"), it.getParseObject("author")?.getString("username"))
  }
}
```

## 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/query | Batched (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

```mermaid
flowchart LR
  accTitle: N plus one waterfall versus batched fetching
  accDescr: 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.
  subgraph P["N+1 waterfall"]
    a["List query"] --> b["Query per record<br/>× N, one after another"]
  end
  subgraph B["Batched"]
    c["List query"] --> d["ONE query for all relations<br/>WHERE id IN (…)"]
  end
  P -.->|"the fix"| B
```

| Fix | How | Reach for it when | Watch for |
| --- | --- | --- | --- |
| Eager loading / include | Declare relations on the query | The screen always needs the relation | Over-including bloats payloads |
| Join | One statement fetches both | Relational engine, report-shaped reads | Row explosion on wide joins |
| Batched IN | Collect keys, fetch once | Any stack, even hand-rolled | One extra round trip (fine) |
| Request-scoped loader | Auto-batch during execution | GraphQL resolvers, layered code | Must 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](https://www.graphql-js.org/docs/n1-dataloader/) — collect keys in one tick, fetch once, memoize per request — is the [industry-standard cure](https://shopify.engineering/solving-the-n-1-problem-for-graphql-through-batching). 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](https://docs.sentry.io/product/issues/issue-details/performance-issues/n-one-queries/). 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](/glossary/relational-queries-document-databases/).
- **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 row | The relation is behind a click |
| The loop is the access pattern | Access is one-record-at-a-time |
| N is page-sized or bigger | N is guaranteed tiny |
| Latency is user-facing | A background task can afford drift |
| You just fixed this bug here | You 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()`](https://docs.parseplatform.org/js/guide/#relational-data) — 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.
