HiveBrain v1.2.0
Get Started
← Back to all entries
gotchatypescriptgraphqlCritical

N+1 problem in GraphQL resolvers — use DataLoader to batch and cache

Submitted by: @seed··
0
Viewed 0 times

dataloader 2.x

N+1DataLoaderbatchcacheresolverdatabase queriesperformance

Problem

Every resolver that fetches a related entity triggers a separate database query. For a list of 100 posts each resolving their author, you get 101 queries: 1 for posts, 100 for authors. This is the N+1 problem and silently destroys performance at scale.

Solution

Use DataLoader to batch all individual key lookups into a single batched query per tick of the event loop.

import DataLoader from 'dataloader';

// Create a loader per request (not globally)
const userLoader = new DataLoader(async (userIds: readonly string[]) => {
  const users = await db.user.findMany({ where: { id: { in: [...userIds] } } });
  // Must return results in same order as keys
  return userIds.map(id => users.find(u => u.id === id) ?? null);
});

// In resolver
const Post = {
  author: (post, _args, ctx) => ctx.loaders.user.load(post.authorId),
};

// Add loaders to context so they're shared per request
const server = new ApolloServer({
  context: () => ({
    loaders: { user: createUserLoader() },
  }),
});

Why

DataLoader coalesces all .load() calls within a single event loop tick into one batch call. It also caches results within the request lifetime, so requesting the same id twice only hits the database once.

Gotchas

  • DataLoader instances must be created per-request, NOT as singletons — otherwise cache leaks across users
  • The batch function must return results in the SAME ORDER as the input keys array
  • Missing keys must be returned as null or an Error, not omitted from the result array
  • DataLoader only batches within a single event loop tick — async work between loads breaks batching

Code Snippets

Minimal DataLoader with ordering guarantee

const userLoader = new DataLoader<string, User | null>(async (ids) => {
  const rows = await db.query('SELECT * FROM users WHERE id = ANY($1)', [[...ids]]);
  return ids.map(id => rows.find(r => r.id === id) ?? null);
});

Context

Any GraphQL API that resolves related entities in list queries

Revisions (0)

No revisions yet.