skip to content

Why should a GraphQL caller put user-supplied values in variables rather than inline literals?

level: seniorimportance: should knowfreq 50%

answer

  1. The text is the identity
  2. Cardinality where there should be one row
  3. Whatever is logged carries it along
  4. Concatenation is the only way in
  5. Constants may stay, data may not

basics

~10 s

Inlining makes the document text change with every value, so two runs of one operation are two different documents. That wrecks document-keyed metrics, allowlists and caches, and drops user data into logged query text.

solid answer

~50 s

The document text is the operation's identity, and almost everything downstream keys off it. With values inlined, `enroll(sectionId: "SEC-4471")` and `enroll(sectionId: "SEC-4472")` are two distinct documents, so per-operation metrics, a registered-document allowlist, a server's parsed-document cache and any response cache all see a fresh key per request instead of one operation with many arguments. At a 1,200-request-per-minute peak that is a six-figure daily cardinality where there should be one row. Inlining also puts user-supplied values into the text that gets logged and attached to traces, so identifiers and free text land where you did not intend and are hard to redact afterwards. And the only way to inline a runtime value is to concatenate strings, which risks a value terminating the literal early and changing what the document asks for. Variables keep one stable document and move the data beside it.

code

graphql · 8 lines
graphql
# inlined: a new document text for every section
mutation { enroll(sectionId: "SEC-4471", status: WAITLISTED) { enrollmentId } }
mutation { enroll(sectionId: "SEC-4472", status: WAITLISTED) { enrollmentId } }

# parameterised: one document text for every section
mutation Enroll($sectionId: ID!, $status: EnrollmentStatus!) {
  enroll(sectionId: $sectionId, status: $status) { enrollmentId }
}

go deeper

for a junior

Know the mechanical difference: a variable is declared on the operation and referenced where the value would go, so the document text stays the same while the values change per request.

for a middle

Be ready to name two concrete things that break when values are inlined — for instance per-operation metrics and a parsed-document cache — rather than saying only that variables are cleaner.

for a senior

Show you can diagnose it on a live system: group requests by operation name, count distinct document hashes, and explain what the cardinality costs the metrics pipeline and the log store.

for a principal

Own the policy and its exception. Decide where the boundary between a constant and data sits for your schemas, and how the rule is enforced across client teams without banning literals that genuinely define an operation.

## The document text is the operation's identity A GraphQL request carries a document, and almost everything a platform builds on top of GraphQL keys off that document's text or a hash of it. When the values are written inline, the text changes every time a value changes, so two requests that a human would call "the same operation" are two different documents to every system downstream. Consider a course-enrolment API. Written with literals, one caller sends: ```graphql mutation { enroll(sectionId: "SEC-4471", status: WAITLISTED) { enrollmentId } } ``` and the next sends the identical text with `SEC-4472`. Written with variables, both send one document and a separate map of values. That is the whole difference, and it decides four separate things. ## What is keyed on the document **Per-operation metrics and traces.** Server and tracing tooling group work by operation, usually by operation name plus a document hash. With literals inlined, one logical operation fans out into as many distinct documents as there are argument values. At a 1,200-request-per-minute peak an inlined-literal mutation can produce well over a hundred thousand distinct document texts in a day, and the per-operation latency view that should have one row has a hundred thousand rows of one sample each. Nothing is measurable at that cardinality. **Registered-document allowlists.** A deployment that only accepts documents registered at build time has to register every distinct text. If the values are inlined, the set of texts is unbounded and the control is unusable — you cannot enumerate a set that depends on runtime data. **Parse and validate caches.** Servers commonly cache the parsed and validated form of a document keyed by its text, because parsing and validating is real work that repeats on every request. A document that is textually unique every time gets no cache hits, so that work is paid in full at every request instead of once per operation. **Response and edge caches.** A cache in front of the server keys on the request; a document whose text varies per value has no shared key to hit. ## Log hygiene The document is the part of the request that is most likely to be logged, attached to a trace span, or included in an error report, because it is the thing a human needs to see to understand what happened. Values inlined in that text go wherever the text goes. Student identifiers, email addresses and free-text notes end up in query logs and vendor traces, and they are extremely hard to remove afterwards because they are inside an opaque string rather than in a labelled field. With variables, the document is stable and value-free, and the variables map is a separate object you can redact, sample or drop wholesale. ## The string-building hazard There is no way to inline a runtime value except to build the document by concatenating text. That is a familiar hazard in a new place. It is not equivalent to SQL injection — the resulting document is still parsed and validated against the schema, so nobody can conjure a field that does not exist or reach past the schema into the datastore. But a value containing a quote can terminate the string literal early and change what the document *asks for*: extra fields in the selection set, a different argument, or a second operation appended after the first. Everything it can reach is schema-legal, which is exactly why it does not look like an attack in the logs. Variables remove the class entirely, because the value never passes through the parser. ## When a literal is still correct The rule is not "never write a literal". A literal is right when the value is part of what the operation *means* and is the same for every caller: a fixed page size, an enum that selects which branch of a filter this named operation represents, a constant flag. Those values belong in the document because they are not data — changing one produces a genuinely different operation, and it should get a different document and a different name. A useful test: if changing the value would not change what you would call the operation in conversation, it is data, and data belongs in variables. There is a small bonus for genuine constants. Because a literal is checked statically against the schema, a typo in a constant fails identically for every caller and shows up the first time anyone runs the document, rather than for one user whose input happened to trigger it. ## Diagnosing it on a running system You rarely get told that clients are inlining values; you find it. Take the server's request log or trace attributes, group by operation name, and count distinct document hashes per name. A healthy name has a handful — one per deployed client version. A name with thousands of hashes is a client building documents by concatenation, and the count itself is the argument you take to that team.

  • When is an inline literal still the right choice for an argument value?
    When the value is part of what the operation means and is identical for every caller: a fixed page size, an enum that selects which branch of a filter this named operation represents, a constant flag. A useful test is whether changing the value would change what you call the operation in conversation. If it would not, the value is data and belongs in a variable.
  • Is inlining a value into the document a comparable risk to SQL injection?
    Not the same class. The resulting document is still parsed and validated against the schema, so nothing can conjure a field that does not exist or reach past the schema. But a value containing a quote can terminate the string literal early and change what the document asks for — extra fields, a different argument, or a second operation appended. Everything it reaches is schema-legal, which is why it does not look like an attack.
  • How would you find out whether your clients are inlining values?
    Group the server's request log or trace attributes by operation name and count distinct document hashes per name. A healthy name shows a handful, roughly one per deployed client version. A name with thousands of hashes is a client concatenating documents, and that count is the evidence you take to the owning team.

Inlining values is printing a fresh form for every applicant; using variables is printing one form with blanks and handing over a separate sheet of answers.

saying these in an interview costs you the question

  • Says literals and variables are interchangeable
  • Thinks servers normalise away differing literal values
  • Believes inlined values never reach logs or traces
  • Builds documents by concatenating user input
  • Claims GraphQL escapes interpolated strings for you
  • Treats one operation name as one document

context