skip to content

Why does interpolating a value into a GraphQL document's text instead of sending a variable hurt the server?

level: middleimportance: must knowfreq 62%

answer

  1. The key is the text, not the meaning
  2. One value equals one new key
  3. Dashboards lose the operation entirely
  4. Nothing pre-registered can ever match
  5. Injection is only the famous reason

basics

~20 s

Every distinct value produces distinct document text, so the server's parse-and-validate cache misses on nearly every request and re-does that work. It also splits per-operation metrics into one series per value, and no document allowlist can ever match.

solid answer

~50 s

A server caches a parsed and validated **document** under a key derived from the exact request text. Concatenating a value into that text - `listing(id: "L-30418")` instead of `listing(id: $id)` - mints a brand-new key per value, so the hit ratio collapses toward zero and every request pays parsing and validation again. Two further costs usually matter more than the CPU. First, **metrics**: servers that aggregate per document hash or signature now emit one series per value, so a single operation's latency and error rate scatter across thousands of one-off rows and no dashboard or alert on that operation works. Second, **any list of known documents** - an allowlist, a registry, a hash handshake - can never match text the client invents at request time. The injection argument for variables is the famous one; these are the other two.

code

graphql · 3 lines
graphql
query { listing(id: "L-30418") { price status } }
query { listing(id: "L-77129") { price status } }
query { listing(id: "L-51260") { price status } }

go deeper

for a junior

Be ready to state the rule and one reason beyond safety: values that change per request go in the variables map, because the server recognises a repeated operation by its exact text.

for a middle

Explain all three costs - cache misses, fragmented per-operation metrics, and the impossibility of pre-registering documents - and be able to say which literals are still legitimate and why.

for a senior

An interviewer expects the detection story: hit ratio as the alarm, distinct document hashes per operation as the confirmation, and the eviction argument for why one bad client degrades everyone else.

for a principal

Own it as a client contract rather than a lint rule. Decide whether the edge accepts arbitrary text at all, and weigh the tooling and deploy coordination that enforcing stable documents imposes on every client team.

## The two shapes Here is the same intent written twice against a real-estate listings graph. Parameterised: ```graphql query ListingPrice($id: ID!) { listing(id: $id) { price status daysOnMarket } } ``` with `{"id": "L-30418"}` in the separate variables map. Concatenated: ```graphql query { listing(id: "L-30418") { price status daysOnMarket } } ``` Both are legal GraphQL documents; the second is not a syntax error, and that is exactly why it survives code review. It is legal and wrong for reasons that show up nowhere in a local test. ## Cost one: the parse-and-validate cache never hits A server keeps the parsed syntax tree and the validation verdict for a document under a key derived from the exact text, precisely because two requests with the same text and the same schema must produce the same tree and the same verdict. Variables are excluded from that key on purpose - they arrive at execution. The concatenated form removes that property. Listing `L-30418` and listing `L-77129` are two different strings, so two different keys, so two entries holding structurally identical trees. Across a catalogue of a hundred thousand listings you get, in the limit, a hundred thousand entries for one logical operation and a hit ratio near zero. Every request re-parses and re-validates. How much that costs depends on document size. For a three-field document it is small. For a generated document with fragments and dozens of selections it is not, because validation walks the document once per rule, and at a peak of 1,200 requests per minute you are paying that walk 1,200 times a minute to reach the same answer. There is a second-order effect that hurts more than the raw CPU: in a **bounded** cache, this traffic evicts the good entries. Well-behaved operations that would have hit forever get pushed out by single-use strings, so the damage is not confined to the misbehaving client. ## Cost two: operation metrics fragment This is the cost that usually gets noticed first in production, and the one interviewers are really probing. Servers commonly aggregate timings, error rates and field usage per **operation**, identified by a hash or normalised signature of the document text - because that is the only identifier that is always present. Interpolated values are part of that text, so each value is a different operation as far as the metrics pipeline is concerned. Instead of one row reading `ListingPrice, p95 84 ms, 1,200 rpm`, you get thousands of rows each seen once, with no meaningful percentile, no error rate, and cardinality that a metrics backend will eventually push back on. Aggregating by `operationName` instead would rescue the metrics half - but concatenated documents are typically written as anonymous operations with no name at all, as in the example above, so in practice both paths are lost together. ## Cost three: nothing can be pre-registered Any scheme that recognises documents ahead of time - an allowlist of permitted operations, a published manifest, a hash-based handshake - depends on the client sending text the server has seen before. Text assembled per request is by construction never text the server has seen before, so the client cannot participate in any of those schemes. A team that concatenates values today cannot turn on document allowlisting tomorrow without changing every call site first. ## And yes, the injection cost The well-known argument is safety: values placed in text must be escaped correctly for GraphQL's string syntax, and a value containing a quote or a brace is a chance to change the shape of the operation rather than the data in it. Variables sidestep that entirely, because a variable value is never parsed as GraphQL. Say it, then move on - an interviewer asking this question on a caching topic is checking whether you know the other three reasons too. ## Which literals are fine Not every literal is a problem. What matters is whether the value **varies per request**. - `first: 25` where the client always asks for 25 - fine. It is part of the operation's shape and the text stays stable. - An enum value the client always sends, such as `sort: PRICE_DESC` on a dedicated operation - fine, for the same reason. - A listing id, a search term, a viewer's account id, a date range from a picker - never. These are per-request data and belong in variables. A useful phrasing for an interview: literals are for what the operation *is*; variables are for what this call *asks about*. ## Detecting it The symptom is the document-cache hit ratio, which should sit near 100% and does not. Confirm it by counting distinct document hashes per named operation over a window: any operation whose distinct-text count grows with traffic is being concatenated somewhere. High-cardinality operation dimensions in the metrics backend give the same signal from the other end.

  • The team argues the CPU saved by the document cache is trivial, so concatenating is fine. What do you say?
    Concede the CPU point and move to the other three. Per-operation metrics collapse, because aggregation keys on the document hash and every value becomes its own operation, so you lose the latency and error rate for that operation entirely. Any future allowlist or registered-document scheme becomes impossible without changing every call site. And in a bounded cache the single-use entries evict the well-behaved operations, so the cost lands on other clients too.
  • Is a constant like first: 25 written directly in the document also a problem?
    No. The test is whether the value varies between requests. A page size the client always sends, or an enum on a dedicated operation, keeps the text stable, so the cache key is stable and the metrics stay aggregated. It is per-request data - identifiers, search terms, viewer ids, date ranges - that must move into variables. Literals describe what the operation is; variables carry what this particular call asks about.
  • How would you find out whether clients are concatenating values, without reading their code?
    Watch the document cache hit ratio first; a healthy client population sits near 100%. Then count distinct document hashes per named operation over a window - an operation whose distinct-text count climbs with traffic is being assembled per request. The metrics backend gives the same signal from the other side, as an operation dimension with runaway cardinality and thousands of series each seen once.

It is the difference between a prepared statement and a string-built one: the parameterised form is recognised as the same statement every time, while the concatenated form is a stranger on every call.

saying these in an interview costs you the question

  • Says the only reason for variables is injection safety
  • Claims the server strips literals before keying the cache
  • Thinks two semantically equal documents share a cache entry
  • Believes metrics still aggregate because the fields are the same
  • Treats every literal as bad, including constant page sizes
  • Assumes the damage is limited to the misbehaving client

context