Skip to content
API Design Learn/GraphQL Foundations
Browsing as a guest. Sign in to save your progress and earn XP as you complete chapters.

Resolvers & the N+1 Problem

8 min read

You'll learn to

  • -Explain how GraphQL resolvers execute per-field, and why that naturally produces the N+1 query problem
  • -Use a DataLoader (or equivalent batching layer) to collapse N+1 queries into a single batched fetch

Every field in a GraphQL schema has a resolver function behind it - a piece of server-side code responsible for producing that field's value. This per-field execution model is what makes GraphQL's flexible querying possible, and it is also the direct cause of GraphQL's most notorious performance problem.

How Resolvers Execute

A query that touches three different resolvers
query {
  users(limit: 10) {
    name
    orders {
      total
    }
  }
}

The `users` resolver runs once, fetching 10 users. Then, naively implemented, the `orders` resolver runs once per user returned - 10 separate calls, each fetching that one user's orders individually. This is the N+1 problem: 1 query for the parent list, plus N queries (one per item) for each item's related data, where a hand-written REST endpoint would typically fetch both in a single join or a single batched query from the start.

The naive resolver that causes N+1
def resolve_orders(user, info):
    # called once PER USER in the result set - 10 users means 10 separate
    # database round trips, one at a time, instead of one batched query
    return db.query("SELECT * FROM orders WHERE user_id = ?", user.id)

DataLoader: Batching Within a Single Request

A DataLoader collects individual load requests that happen within the same tick of the event loop (effectively, within the same GraphQL request) and batches them into a single call, instead of dispatching each one immediately. Rather than 10 separate "get orders for user X" calls, all 10 user IDs are collected first, then issued as one "get orders for users [1,2,3...10]" query.

A batched resolver using a DataLoader-style pattern
# The DataLoader batches every load() call made during this request into
# one function call, keyed by all the collected IDs at once:
order_loader = DataLoader(batch_load_fn=lambda user_ids:
    db.query("SELECT * FROM orders WHERE user_id IN ?", user_ids)
)

def resolve_orders(user, info):
    return order_loader.load(user.id)   # queued, not executed immediately

# 10 users -> 10 load() calls queued -> exactly ONE batched query executed

The key behavioral shift: `.load()` doesn't execute anything immediately - it queues the request and returns a promise/future that resolves once the batch actually runs. GraphQL's execution engine naturally creates the opportunity for this batching, since it already resolves sibling fields (like every user's `orders` field) before waiting on any of them individually.

DataLoader instances must be created fresh per request, not shared globally - a DataLoader also caches results within its lifetime, and a shared, long-lived instance would serve stale or, worse, cross-request-leaked cached data to the wrong caller.

Interview Signal is part of Pro

See a real weak answer next to a real strong one for this exact topic.

Quiz is part of Pro

Test what you just read with a short quiz, and bank the XP.

Ready to Build This?

Design The Resolver Graph in the API Design Lab's GraphQL Mastery act.

ScaleDojo Logo
Initializing ScaleDojo