skip to content

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

level: middleimportance: should knowfreq 58%

answer

  1. Levels multiply, siblings add
  2. Product of the list sizes above
  3. Count parents per level, not fields
  4. The deepest level dominates the total

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.

solid answer

~50 s

Walk the document level by level and, at each level, ask two things: how many parent objects exist here, and which fields at this level reach a backend. The parent count at any level is the product of the list sizes above it, so nesting multiplies rather than adds. For a page of 47 courses, one backend-resolved field on the course costs 47 calls; a nested page of 12 enrolments per course puts 564 enrolment objects in play, and one backend-resolved field on the enrolment costs 564 calls. Adding the term, the course list and the 47 enrolment-list calls, that document costs 660 round trips. The ceiling for the schema is the product of the maximum page sizes each list field permits, which is why an unbounded list field anywhere in a chain makes the ceiling unbounded too.

code

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

go deeper

for a junior

Recall that nesting multiplies. If a page of 47 items each holds 12 children, there are 564 children, and any field on a child that fetches something runs 564 times. Know that plain scalar fields on an already-loaded object are free.

for a middle

Be able to tally a given document out loud, level by level: parent count at each level is the product of the list sizes above it, times the fields at that level that reach a backend. Interviewers hand you a document and expect the number.

for a senior

Show that you reason about the ceiling, not just one execution: the worst case is the product of the permitted page sizes along the deepest list chain, so a single uncapped list field makes the whole product unbounded.

for a principal

Own the composition argument — per-field page caps chosen independently multiply into absurd totals. Be ready to say how you would pick limits from real client documents and where enforcement belongs relative to schema-design review.

## The counting rule There is one rule, applied level by level from the root: > **calls at a level = (number of parent objects at that level) × (fields at that level whose resolver reaches a backend)** and > **number of parent objects at a level = the product of the list sizes on the path above it** That second line is the whole story of nested amplification. Levels do not add, they multiply, because the executor runs a sub-selection against every item of every list on the way down. ## A worked example ```graphql query TermRoster { term(code: "2026-FALL") { # 1 object courses(first: 47) { # 47 objects title instructor { displayName } # resolved per course enrolments(first: 12) { # 12 per course => 564 objects grade student { displayName } # resolved per enrolment } } } } ``` Tally it: | Level | Parent objects | Backend-resolved fields | Calls | |---|---|---|---| | root | 1 | `term` | 1 | | term | 1 | `courses` | 1 | | course | 47 | `instructor` | 47 | | course | 47 | `enrolments` | 47 | | enrolment | 564 | `student` | 564 | Total: **660 backend calls for one document**. `title` and `grade` contribute nothing — they are read off objects already in memory by default resolution. Notice how little of the document is responsible. Two nested list fields and two object-valued fields produced 660 round trips, and 564 of them — 85% — came from a single innocuous line, `student { displayName }`, sitting two levels down. ## Why depth is the dangerous axis Adding a sibling field at a level you are already at is additive: a second backend-resolved field on the course adds 47 calls. Adding a *level* is multiplicative: it takes the parent count you already had and multiplies it by the new list's size. That asymmetry is why reviewers who count fields miss the problem entirely. The number to watch is the running product of list sizes, not the line count. It also means the last level dominates. In the tally above, everything above the enrolment level accounts for 96 calls and the bottom level accounts for 564. Optimising the top of a document is almost always the wrong end. ## The caller sets the scale, the schema sets the ceiling Each of those list sizes came from an argument the caller chose. The same document with `courses(first: 3)` and `enrolments(first: 2)` costs 1 + 1 + 3 + 3 + 6 = 14 calls. Nothing about the server changed; the multiplier did. The worst case a schema permits is therefore the product of the maximum page size each list field allows, multiplied along the deepest chain of list fields the schema can express. Two things follow: - A list field with **no** page-size cap makes the ceiling unbounded, and one uncapped field anywhere in the chain is enough to do it — the product has an unbounded factor in it. - Caps compose badly. A cap of 100 at each of three levels reads as reasonable per field and permits a million leaf objects. Sensible per-field limits are not sensible in composition, which is why the caps that matter are usually chosen by looking at the product along real client documents rather than field by field. (Turning that ceiling into an enforced pre-execution budget is an abuse-control concern with its own machinery; the counting here is what tells you whether such a control is needed and where the product blows up.) ## Recursion and cycles GraphQL type graphs are usually cyclic — a course reaches its enrolments, an enrolment reaches its student, a student reaches their enrolments again. A document may walk that cycle repeatedly, and each traversal is another multiplicative level. This is how a document that looks short on the page can express a product of five or six list sizes. ## Doing the count in practice You rarely count by hand for long. Two habits do it better: 1. **Instrument per field.** Have the server tally resolver invocations keyed by the field's path in the document, then print the tally for one request. The 564 will be sitting there on the `student` row, and the ratio between a row and its parent's row is exactly the fan-out at that level. 2. **Reason on the response.** The number of objects at a level in the response body is the parent count for the level beneath it. If the response contains 564 enrolment objects, any backend-resolved field on the enrolment cost 564 calls — no tracing needed to know that. And the arithmetic explains a class of surprises that otherwise looks random: a client adds one field, the document's text grows by three words, and backend load multiplies. That is not a mystery. It is the product of the list sizes above the field they added.

  • Which costs more — adding a second backend-resolved field at the level you are already on, or adding one nesting level?
    The nesting level, by a wide margin. A sibling field adds calls equal to the current parent count, so it is additive. A new level multiplies the parent count by the new list's size and then charges every backend-resolved field beneath it against that larger number. Siblings add; depth multiplies.
  • If every list field caps its page size at 100, is the document's cost bounded to something reasonable?
    Bounded, yes; reasonable, no. Caps compose by multiplication, so three nested list fields capped at 100 each permit a million leaf objects. Per-field caps that look sensible in isolation are chosen without reference to each other, which is why the number worth checking is the product along the deepest chain of list fields the schema allows.
  • How do you get the parent count for a level without instrumenting the server?
    Read it off the response. The number of objects a level contributes to the response body is exactly the parent count for the level below it, so if the body holds 564 enrolment objects, any backend-resolved field on the enrolment type was invoked 564 times. It is a quick estimate you can make from a captured response alone.

It is compound interest, not a running total: each nesting level applies a fresh multiplier to everything accumulated above it.

saying these in an interview costs you the question

  • Adds the list sizes together instead of multiplying them
  • Counts fields in the document rather than parent objects
  • Treats each nesting level as a constant extra cost
  • Thinks a per-field page cap bounds a nested document sensibly
  • Ignores that scalar fields on loaded objects cost nothing
  • Optimises the top level while the bottom level dominates

context