GraphQL N+1 and DataLoader Lab
Trace nested GraphQL resolvers, count N+1 SQL queries, batch users and comments with DataLoader, and discover why a loader cache must stay inside one request.
Category: DevOps
What You Will Learn
- Identify the GraphQL N+1 query pattern across nested list fields
- Trace GraphQL resolver levels and their event-loop batching boundaries
- Use DataLoader to batch user and comment lookups into IN queries
- Understand DataLoader key deduplication and per-loader caching
- Prevent cross-request data leaks by creating loaders per request
Topics covered: graphql, dataloader, n-plus-one, database, caching, api, interactive
// simulator
GraphQL N+1 and DataLoader Lab
Trace nested GraphQL resolvers, count N+1 SQL queries, batch users and comments with DataLoader, and discover why a loader cache must stay inside one request.
Goal: Ask for posts and their authors, and count the queries. Press Run query with Post authors on: one query for the posts, then one per author.
Query fields and server
Configure the request, then run the query.
Requested query tree
- Query.posts(first: 10): [Post]
- Post.author: User
- User.name
- User.email
- Post.comments: [Comment] (not requested)
- Comment.author: User (not requested)
- User.name (not requested)
- User.email (not requested)
- Comment.author: User (not requested)
- Post.author: User
About the GraphQL N+1 and DataLoader Lab
What you'll practise
- Counting the database work hidden behind a nested GraphQL selection set
- Seeing one resolver level finish before the next nested level starts
- Batching same-tick user and comment lookups through DataLoader
- Distinguishing a request-scoped cache from a dangerous shared cache
How it works
- Toggle fields, loaders and cache scope, then run the same blog query as Ada or Bo.
- The trace follows each resolver level, shows its event-loop tick, and records the SQL sent at that level.
- The lab uses a small deterministic blog data set, so the query counts and cache result stay repeatable while you compare configurations.
Overview
GraphQL makes a nested response look like one request, but each field resolver can still issue database work. This lab follows Query.posts(first: 10) for a blog with ten posts, three comments per post, and six users. With post authors only, naive resolvers send one posts query and one user query for every post, for eleven queries. Add comments and each comment author, and the same pattern becomes fifty-one queries. The execution trace groups resolver work by level: all post author resolvers call load in one tick, comments resolve at the next level, and comment authors run only after comments resolve. Turn on the User DataLoader to coalesce same-tick user keys into one ordered batch, with duplicate keys loaded once through its cache. Turn on the Comments DataLoader too and the whole nested page uses four queries in this model. Finally, run as Ada, share loaders across requests, then run as Bo. The shared user cache returns Ada’s cached object, including the email visible when Ada loaded it. Switch back to Per request and Bo receives null for Ada’s email. The model leaves out awaits between loads, which would split DataLoader batches.
Try this
- Leave Post authors on, keep both DataLoader controls off, and press Run query to count the posts-and-authors N+1 work.
- Turn on Comments and Comment authors, then press Run query before and after enabling Use User DataLoader and Use Comments DataLoader.
- Choose Shared by all requests, run as Ada, switch Viewer: Ada to Viewer: Bo, press Run query, then choose Per request and run it again.
Frequently asked questions
- Why does a nested GraphQL query create an N+1 problem?
- The posts resolver gets the first result set, then each Post.author resolver can independently ask for its user. With ten posts, that is one posts query plus ten user queries when resolvers are naive. Nested comments add another resolver level and more repeated lookups.
- What does DataLoader batch in this lab?
- A DataLoader collects load calls made in one event-loop tick and invokes its batch function once with the keys. The users batch emits one IN query for unique uncached user IDs, while the comments batch emits one IN query for post IDs. Its batch function must return values in the same order and length as the requested keys.
- Why must a DataLoader be created for each request?
- Its cache memoizes values for the lifetime of that loader. Reusing one loader for two viewers lets the second request receive an object built for the first viewer, which can bypass viewer-specific field rules. A fresh loader per request keeps the cache and its authorization context separate.
Try next
// simulator
Idempotency and Retry Lab
Lose payment responses, replay idempotent requests, compare backoff with full jitter during an outage, and move exhausted jobs to a dead-letter queue.
// simulator
Preview Environment Simulator
See how a pull request becomes a temporary copy of your app, complete with a private URL, safe test data, review checks, and automatic cleanup.
// game
Heroku-Style Name Generator
Generate names the way Heroku, Docker, Kubernetes, and petname do, see how the word lists combine, and learn when the birthday problem makes random names collide.