Without batching, one innocent-looking query can trigger hundreds of database calls.
Resolvers run per object. Fetch 50 books and select author { name }, and the naive Book.author resolver runs 50 separate lookups: one query for the list plus N for the authors, the classic N+1 problem.
DataLoader (the dataloader package, also ported to most languages) fixes it. You create a loader with a batch function that takes an array of keys and returns a promise of values in the same order. Resolvers call loader.load(key); the loader collects every key requested in the same tick of the event loop and calls the batch function once.
Rules that keep it correct:
- The batch function must return exactly one result per key, in key order (use
nullor anErrorfor misses). - Create loaders per request (in the context factory). Their memoization cache would otherwise serve stale data and leak data between users with different permissions.
- Loaders cache by key within the request, so the same author loaded twice hits the database once.
Batching works because promises resolve as microtasks: DataLoader schedules its dispatch after the current batch of resolver calls. Alternatives include joining in the parent resolver using info look-ahead, or query planners like those in Hasura and PostGraphile.
Example
import DataLoader from "dataloader";
const db = { calls: 0,
async authorsByIds(ids) { this.calls++; return ids.map((id) => ({ id, name: "Author " + id })); },
};
function createContext() {
return {
authorLoader: new DataLoader(async (ids) => {
const rows = await db.authorsByIds(ids);
const byId = new Map(rows.map((r) => [r.id, r]));
return ids.map((id) => byId.get(id) ?? null); // same order as ids
}),
};
}
const resolvers = { Book: { author: (book, _a, ctx) => ctx.authorLoader.load(book.authorId) } };
const ctx = createContext();
const books = [{ authorId: "a1" }, { authorId: "a2" }, { authorId: "a1" }];
const authors = await Promise.all(books.map((b) => resolvers.Book.author(b, {}, ctx)));
console.log(authors.map((a) => a.name), "db calls:", db.calls);
// [ 'Author a1', 'Author a2', 'Author a1' ] db calls: 1Remember
- N+1: one query for the list plus one per item
- DataLoader batches keys requested in the same tick
- Batch functions return results in key order
- One loader instance per request
Watch out for
- A global DataLoader shared across requests: stale data and cross-user leaks.