Why does a GraphQL depth cap need a field-count cap and a body-size cap alongside it?
answer
- Depth measures one dimension only
- Shallow can still be enormous
- A reused fragment is a multiplier
- Validation runs after the parser has finished
- Bytes are checked before anything is parsed
basics
~20 sA depth cap bounds only the longest path. A shallow document can still select hundreds of fields at every level, and any document must be parsed in full before validation runs, so breadth and raw bytes each need their own limit.
solid answer
~40 sThe three limits catch three different document shapes. **Depth** stops a long thin chain. **Total field count**, a second validation rule, stops a two-level document that names eight hundred fields — and it must count the document *after fragment expansion*, because a 2 KB request that spreads one forty-field fragment at three hundred sites expands to roughly twelve thousand fields. **Body size** is a byte limit enforced before parsing, and it is the only one that protects the parser: depth and field-count rules run at validation, by which point the whole document has already been tokenised into an AST. All three are conventions, not specification requirements. None substitutes for another — a small body can expand into a huge document, and a large body can be a perfectly legitimate first-party query.
code
graphql · 14 linesfragment Vitals on Animal {
id tag birthDate weightKg heightCm bodyConditionScore
}
query Wide {
herd(id: "HRD-318") {
animals {
...Vitals
sire { ...Vitals }
dam { ...Vitals }
offspring { ...Vitals }
}
}
}go deeper
Know that depth is only one way a request can be huge: a document can be shallow and still ask for hundreds of fields, or simply be a very large body. Naming the three separate limits is enough at this level.
Explain the mechanics of each: the field-count rule must count the flattened document with fragments expanded, and the byte cap must run before parsing because validation only happens once an AST already exists. Say that none of it is specified.
Demonstrate that you derive the numbers from measured traffic rather than defaults, and that you can articulate what each cap still misses — most importantly, that field count says nothing about how many records each field is resolved against.
Own the layering decision: which limits sit in a proxy on bytes, which sit in the server's validation stage, and what the organisation's policy is when a legitimate client outgrows a cap. Be ready to justify the cost of the safety margin.
## Three different documents, three different limits A depth cap answers exactly one question: how long is the longest root-to-leaf path in this document? That leaves two other ways for a single request to be enormous, and each needs its own control. Consider a pedigree API for a breed registry. **Deep.** `sire { sire { sire { … } } }` — a long thin path. Caught by a depth cap. **Wide.** One selection set naming several hundred fields, all at depth two. A registry `Animal` type with measurements, health records, lineage summaries and show results can easily expose two hundred scalar fields, and the document may select every one of them, on every element of a list. Depth two, cost enormous. A depth cap never sees it. **Bulky.** A multi-megabyte request body. The parser must read and allocate the whole thing before any validation rule — including the depth rule — has an opinion. The document may not even be valid; you paid for the parse regardless. ## The field-count cap, and why fragments are the trap The breadth control is a limit on how many fields the document selects in total, applied as an extra validation rule alongside the depth rule. Walk the operation, count selected fields, reject above a threshold. The essential subtlety is that it must count the **flattened** document, with fragment spreads expanded at every site they appear. Fragments are reusable by design and cheap to write, which makes them a multiplier: ```graphql fragment Vitals on Animal { id tag birthDate weightKg heightCm bodyConditionScore ... } query Wide { herd(id: "HRD-318") { animals { ...Vitals sire { ...Vitals } dam { ...Vitals } offspring { ...Vitals } } } } ``` A 2 KB document that defines one forty-field fragment and spreads it at three hundred sites expands to roughly twelve thousand selected fields. Counting the fragment definition once, or counting literal field tokens in the text, under-reports by two orders of magnitude — and an attacker who notices will move everything into fragments. Count after expansion, or the rule is decorative. Note also what a field-count cap does *not* measure: how many *records* each selected field is resolved against. Selecting eight fields on a list that returns four thousand rows is eight fields by this measure and thirty-two thousand resolver invocations in reality. Bounding that is the job of list-page limits and weighted cost scoring, which is a different control with a different model. ## The body-size cap, and why it must come first A byte limit on the raw request body is the crudest of the three and the only one that protects the parser itself. Every GraphQL limit implemented as a validation rule shares one structural weakness: validation runs *after* parsing, so by the time the depth rule and the field-count rule get a vote, the server has already tokenised the document and built an abstract syntax tree for all of it. A 30 MB body of legal-but-absurd selections costs real CPU and real allocation before anything rejects it. Under a sustained peak of around 1,200 requests per minute, that pre-validation work alone is enough to saturate a service. So the body-size cap is enforced at the HTTP layer, on bytes, before parsing begins — usually as a maximum request size in the server or in a reverse proxy in front of it. It is dumb by design: it cannot tell a legitimate large document from an abusive one, which is why the number is set generously, well above the largest real client document, and the smarter rules do the discriminating afterwards. There is a corollary worth stating out loud in an interview: **body size is a poor proxy for document cost in both directions**. The fragment example above is 2 KB and expands to twelve thousand fields; a genuinely large query document from a first-party client may be 40 KB and entirely reasonable. The three limits are not redundant, and none of them substitutes for another. ## Where each one sits The ordering follows from what each control can see: 1. **Bytes**, before parse — the only limit that runs before the server has spent anything. 2. **Depth and total field count**, at validation — the document has been parsed, so its structure is known, but no resolver has run and no backend has been touched. 3. **Weighted cost**, also at validation but with a schema-aware model — the refinement, when the flat counts prove too blunt. All three are conventions. The GraphQL specification defines validity, not resource policy, and mandates none of these numbers; the GraphQL over HTTP working draft is likewise about transport, not limits. What *is* specified is only the shape of the refusal, once you implement one as a validation rule: an `errors` entry and no `data` key. ## Choosing the numbers Derive them from observed traffic, not from a blog post. Record the depth, post-expansion field count and body size of real documents over a representative window, then set each cap with headroom above the observed maximum for legitimate clients. Caps that are tighter than real usage do not make you safer; they turn an availability control into an availability incident, and they generate the false rejections that get the whole limiter switched off in a hurry.
- Why can a small request body still represent a very large document?Because fragments are expanded at every spread site. A forty-field fragment defined once and spread three hundred times is a couple of kilobytes of text and roughly twelve thousand selected fields after expansion. That is exactly why a field-count rule must count the flattened document rather than the tokens in the request text, and why a byte cap is not a substitute for one.
- If the body-size cap is so crude, why bother with it at all?Because it is the only limit that runs before the server spends anything. Depth and field-count rules are validation rules, and validation happens after the whole document has been parsed into an AST — so a multi-megabyte body costs real CPU and allocation before any smart rule gets a vote. The byte cap is set generously, well above the largest legitimate document, purely to stop that.
- Does a field-count cap tell you how much backend work the document will cause?No. It counts fields in the document, not resolutions. Eight fields selected on a list that returns four thousand rows is eight by this measure and thirty-two thousand resolver invocations in practice. Bounding that needs page-size limits on list arguments and a weighted, schema-aware cost model, which is a separate control.
saying these in an interview costs you the question
- Treats a depth limit as sufficient protection on its own
- Counts fields from the request text instead of after expansion
- Says body size is a reliable proxy for document cost
- Thinks the specification mandates these limits
- Believes a validation rule can reject a body before parsing
- Confuses selected field count with resolver invocation count