skip to content

questions

3

In GraphQL, why does selecting a child field over a list of N items fire N separate backend calls?

level: juniorimportance: must knowfreq 82%

answer

  1. Count calls, not fields
  2. One resolver, many parents
  3. The list length sets the call count
  4. One call for the list, N for children

basics

~20 s

GraphQL execution runs a field's resolver once per parent object. If a list holds N parents, the child field's resolver runs N times — so one document costs the call that fetched the list plus N more.

solid answer

~50 s

The execution algorithm in the GraphQL specification resolves a selection set **against one object at a time**. When a field returns a list, the executor walks the list and executes the sub-selection separately for each item, so a child field's resolver is invoked once per element. That resolver has no idea it is one of 47 siblings; if it reaches a data store itself, the request costs 1 call for the list plus N for the children — the shape people call N+1. Nothing in the specification coalesces those calls: per-item resolution is specified behaviour, and merging the child lookups is a convention layered on top. The tell is that the call count tracks the page size the caller asked for, not the size of the document. A field read straight off the parent object costs nothing extra; only fields whose resolver reaches a backend amplify.

code

graphql · 10 lines
graphql
query TermRoster {
  term(code: "2026-FALL") {
    courses(first: 47) {
      title
      instructor {
        displayName
      }
    }
  }
}

go deeper

for a junior

Be ready to state the shape out loud: one call fetches the list, then the child field's resolver runs once per item. Recall that a field read straight off the parent object costs nothing, and that nothing in GraphQL merges those calls for you.

for a middle

Explain the mechanics: execution is defined per object, list completion executes the sub-selection per item, and default resolution reads a property. Be able to point at which fields in a given document actually reach a backend and which are free.

for a senior

Show that you reason in round trips under real page sizes. Interviewers expect you to note that concurrency repacks the calls without reducing them, that a connection pool serialises the burst anyway, and that the count is set at runtime by the caller's arguments.

for a principal

Own the framing that the schema author fixes the shape of the cost while the caller fixes its scale. Be ready to argue where responsibility for bounding that sits — field-level fetch discipline, mandatory pagination on list fields, or a platform-level budget.

## Execution is defined per object, not per document GraphQL's execution algorithm is specified in terms of a pair: a selection set and a single source object. For every field in that selection set the executor invokes the field's resolver with that object as the parent, then *completes* the returned value against the field's declared type. When the declared type is a list, completion walks the list and, for each item that is itself an object, runs the field's sub-selection set against that one item. There is no step anywhere in that algorithm where the executor gathers all the items of a list and hands them to a child field's resolver together. The child resolver's contract is one parent in, one value out. That is not a quirk of any particular server — it is what the specification describes, and every conforming implementation behaves this way. ## The arithmetic that falls out Take a course-enrolment graph. A client asks a term for its courses and, for each course, the instructor: ```graphql query TermRoster { term(code: "2026-FALL") { courses(first: 47) { title instructor { displayName } } } } ``` If `instructor` is a foreign key on the course row and its resolver looks the instructor up by id, the executor invokes that resolver 47 times — once per course — and each invocation issues its own lookup. Add the single call that fetched the 47 courses and the request costs 48 backend calls. `title`, by contrast, costs nothing: it is read directly off the course object the list resolver already returned, which is what the specification's default resolution does when a field has no explicit resolver. So the multiplier is not "how many fields the document mentions". It is: **for each field whose resolver reaches a backend, how many parent objects that field is resolved against**. A document with twenty scalar fields on one object costs one call. A document with one object-valued field over a list of 200 costs 201. ## Why the count is set by the caller, not the schema The number of parents is a runtime property. It comes from the page size the caller passed (`first: 47`), or from however many rows an unbounded list happened to return that day. The same stored document therefore costs a different amount on every execution. This is the single most important consequence for anyone operating a GraphQL endpoint: the schema author fixes the *shape* of the cost, the caller fixes the *scale* of it, and the server discovers the total only while it is executing. It is also why the failure mode is invisible in a smoke test. With one course in the fixture the request makes two calls instead of one; nobody notices a doubling. With a realistic page it makes forty-eight, and the latency is roughly the list call plus N times the per-child call. ## Concurrency does not remove the calls A common half-answer is that the executor runs the sibling resolvers in parallel, so the fan-out does not matter. The specification does permit fields in the same selection set to be executed in any order and, for query operations, concurrently. That changes the *wall-clock shape* — 47 lookups may overlap rather than queue — but it does not change the *count*. Forty-seven concurrent lookups still occupy forty-seven connections from a pool that probably holds eight, so in practice they serialise on the pool anyway, and a bursty fan-out is often harder on a shared data store than the same work spread out. The number of round trips is the thing to reason about; concurrency only decides how they are packed in time. ## What is spec and what is convention Worth separating cleanly, because interviewers probe it: - **Specified**: per-object resolution, per-item execution of a list's sub-selection, default resolution reading a property of the parent, permission to execute sibling fields concurrently. - **Convention**: everything that fixes the amplification. Coalescing the N child lookups into one bulk lookup per tick — the pattern named DataLoader — is an implementation practice with no standing in the specification. So is prefetching children while resolving the parent, and so is reading the incoming selection set to decide what to fetch up front. Saying "GraphQL batches this for you" is simply wrong, and it is the answer that ends a screening call. Nothing batches unless the server was written to batch. ## Recognising it in the wild Three symptoms travel together. Latency scales with the requested page size rather than with document length. The data store's statement log shows the same statement text repeated within one request with only the bound id changing. And adding one innocuous-looking field to an existing client document multiplies backend load, because that field is resolved once per row of a list that is already there.

  • Does every field in the selection set add to the call count?
    No. Only fields whose resolver reaches a backend do. A field with no explicit resolver falls back to default resolution, which reads a property of the parent object already in memory, so twenty scalar fields on one object still cost zero extra calls. The fields to audit are the object-valued and list-valued ones that fetch something.
  • The executor runs sibling fields concurrently — doesn't that make the fan-out harmless?
    It changes the timing, not the count. The specification allows fields in a selection set to execute in any order and concurrently for queries, so 47 lookups may overlap, but 47 round trips still happen. They will usually queue on a connection pool of eight, and a sudden burst can be harder on a shared data store than the same calls spread out.
  • Is the N+1 shape something the GraphQL specification tells servers to avoid?
    No. The specification defines per-item execution and says nothing about how a resolver obtains its data. Coalescing the N child lookups into one bulk lookup — the DataLoader pattern — is a widespread convention implemented by servers and application code, not a specified requirement. A candidate who attributes batching to the spec has the layering wrong.

It is a mail merge, not a bulk order: the executor hands the child resolver one address at a time, so a hundred recipients means a hundred separate trips to the filing cabinet.

saying these in an interview costs you the question

  • Claims GraphQL automatically batches identical child lookups
  • Says the cost tracks the number of fields in the document
  • Assumes one HTTP request means one database query
  • Thinks concurrency between sibling resolvers removes the extra calls
  • Blames the caller rather than the per-item execution model
  • Believes scalar fields on a loaded object each cost a call

context

open as a page

How do you count the backend calls a GraphQL document makes when list fields nest?

level: middleimportance: should knowfreq 58%

basics

~10 s

Multiply down the tree. The parent count at a level is the product of the list sizes above it, so 47 courses each holding 12 enrolments means an enrolment's fetching field runs 564 times.

open as a page

Your GraphQL endpoint passes every test but times out in production — how do you prove field fan-out is the cause?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Count resolver invocations per field path for one real request and compare each field's count with its parent's. A ratio matching the list length — not a slow single call — proves per-item fan-out, which one-row fixtures cannot reveal.

open as a page