100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
GraphQL

DataLoader and the N+1 Problem

Understand why naive GraphQL resolvers trigger the N+1 query problem and how Facebook's DataLoader batches and caches requests to fix it.

ResolversIntermediate11 min readJul 10, 2026
Analogies

How the N+1 Problem Arises

The N+1 problem happens when resolving a list of N items requires 1 query to fetch the list, plus N additional queries, one per item, to resolve a nested field on each. Querying 40 books and each book's author with a naive resolver like author: (parent) => db.authors.findById(parent.authorId) issues 1 query for the books and then 40 separate queries, one per book, to fetch each author, even when many books share the same author and the same row gets fetched repeatedly. This pattern is invisible in small test data but becomes a severe production bottleneck as list sizes grow.

🏏

Cricket analogy: A scorer who looks up each of the 11 batters' career averages individually in a separate reference book, one lookup per player, instead of pulling the whole squad's stats sheet at once, mirrors the N+1 pattern.

What DataLoader Actually Does

DataLoader, the library open-sourced by Facebook, solves this with batching and per-request caching. Instead of executing a fetch the moment .load(id) is called, it collects every .load() call made during the current tick of the event loop into a queue, then on the next tick invokes a single user-supplied batch function once with the full array of collected keys, which is expected to return results in the same order as the input keys. If the same key is requested twice within a request, DataLoader also caches the result and returns the cached promise instead of calling the batch function again, so duplicate authorIds across 40 books collapse into a single fetch of unique IDs.

🏏

Cricket analogy: A team analyst who waits until every player has submitted their fitness data request, then sends one consolidated batch request to the medical team, is exactly how DataLoader queues .load() calls before batching.

javascript
import DataLoader from 'dataloader';

function createAuthorLoader(db) {
  return new DataLoader(async (authorIds) => {
    const authors = await db.authors.findByIds(authorIds); // ONE query
    const byId = new Map(authors.map((a) => [a.id, a]));
    // Must return results in the SAME ORDER as the input keys
    return authorIds.map((id) => byId.get(id) ?? null);
  });
}

// resolvers.js
const resolvers = {
  Book: {
    author: (parent, args, context) => context.loaders.author.load(parent.authorId),
  },
};
// context is built fresh per request, so create a new loader per request too:
// context: () => ({ loaders: { author: createAuthorLoader(db) } })

One Loader Instance Per Request, Never Global

DataLoader instances must be created fresh inside the context function for every incoming request, never as a module-level singleton shared across requests. Because DataLoader caches by key for the lifetime of the instance, a globally shared loader would leak cached results and stale data between unrelated users and requests, and worse, could serve one user's private record to a different user who happens to request the same key. The batching benefit only needs to span a single request's execution, so a per-request instance is both correct and sufficient.

🏏

Cricket analogy: A fresh scorebook opened for every new match, never carrying entries over from the last match, mirrors creating a new DataLoader per request rather than reusing one across requests.

A DataLoader instance reused across requests will serve stale or cross-user cached data. Always instantiate loaders inside the per-request context factory function, never at module scope.

  • N+1 happens when resolving N list items each triggers its own separate data-source query.
  • DataLoader batches every .load() call made in a single event-loop tick into one batch function invocation.
  • The batch function must return results in the same order as the input keys array.
  • DataLoader also caches results per key for the life of the instance, deduplicating repeated requests within a request.
  • Loader instances must be created fresh per request inside the context factory, never as a module-level singleton.
  • Reusing a loader across requests risks leaking stale or cross-user data.
  • DataLoader turns N separate database round trips into a single batched round trip per unique key set.

Practice what you learned

Was this page helpful?

Topics covered

#GraphQL#GraphQLStudyNotes#WebDevelopment#DataLoaderAndTheN1Problem#DataLoader#Problem#Arises#Actually#APIs#StudyNotes#SkillVeris