skip to content

Is the persistedQuery extension defined by the GraphQL specification, or is it a convention?

level: juniorimportance: nice to knowfreq 24%

answer

  1. The standard reserves the room, not the furniture
  2. Two ends agree; nothing enforces it
  3. A map for implementation-defined data
  4. Unknown extensions are ignored, not rejected
  5. Version 1 plus a 64-character digest

basics

~20 s

A convention, not specification. GraphQL reserves an extensions map for implementation-defined data and defines nothing named persistedQuery; the entry carrying a version number and a SHA-256 hash is a shape clients and servers agree on privately.

solid answer

~50 s

The GraphQL specification reserves `extensions` as a map for implementation-defined data and then says nothing about what goes in it, so nothing named `persistedQuery` is specified anywhere — not in the specification, not in the GraphQL over HTTP working draft. Automatic Persisted Queries is a **convention**: a client and a server both hard-code the same shape, `{"version": 1, "sha256Hash": "<64 hex chars>"}`, and the same signal for an unknown hash, so they interoperate without a standard telling either of them to. There is no negotiation and no capability advertisement, so both ends must independently agree on the entry name, the version integer, the algorithm and encoding, exactly which text was hashed, and the not-found message string. A server that does not implement it simply ignores the unknown extension — and since the hash-first request carries no `query`, that request then fails for having no document at all.

code

json · 10 lines
json
{
  "operationName": "ClinicDaySlots",
  "variables": { "clinicId": "clinic-4417", "date": "2026-09-14" },
  "extensions": {
    "persistedQuery": {
      "version": 1,
      "sha256Hash": "fc7cd86c2ce9a1a1004bb386539671e7bd77d84114ee78113d809324ee47b77a"
    }
  }
}

go deeper

for a junior

Recall that this is a convention rather than specified behaviour, and that the extension carries just a version number and a SHA-256 hash of the document. Knowing that unknown extensions are ignored rather than rejected is the useful extra.

for a middle

Explain what the two ends must agree on with no negotiation available: entry name, version, algorithm and encoding, the exact bytes hashed, and the not-found message string. Be ready to describe the failure a non-supporting server produces.

for a senior

Show the habit of separating specified behaviour from convention before relying on either, and be able to say which layer to read — a server's documentation, not the specification — when asked whether a client will interoperate with a given server.

for a principal

Own the wider judgement: which unspecified conventions your platform standardises on, how you keep client and server dialects from drifting apart across teams, and what you accept in exchange for depending on behaviour no standard guarantees.

## The specification reserves a room and leaves it empty The GraphQL specification is precise about exactly one thing here, and it is not persisted queries. It reserves an `extensions` entry as a map for implementation-defined data — a place implementations may put whatever they like — and then declines to say what may go in it. The response envelope's `extensions` entry is reserved that way by the specification itself; an `extensions` map among the *request* parameters is carried by the GraphQL over HTTP working draft. Neither document defines an entry called `persistedQuery`, a hash algorithm, a version number, or the error a server returns when it does not recognise a hash. So the answer to "is this in the spec?" is no. The interesting half is what fills the gap. Automatic Persisted Queries is a **convention** — an agreed wire shape that a client library and a server implementation each hard-code so that they interoperate, without any standard obliging them to. The shape is small enough to state completely: ```json { "operationName": "ClinicDaySlots", "variables": { "clinicId": "clinic-4417" }, "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "fc7cd86c2ce9a1a1004bb386539671e7bd77d84114ee78113d809324ee47b77a" } } } ``` That is the entire contract on the wire. `version` is an integer that has been `1` for as long as the convention has existed. `sha256Hash` is the lowercase hexadecimal SHA-256 digest of the operation document — 64 characters, always. ## What both ends must agree on, and what nothing checks Because nothing specifies it, there is no conformance test, no negotiation step and no capability advertisement in the protocol. Every item below is an agreement two independent implementations happen to share: * **The name of the extension entry.** A server looking for `persistedQuery` will never find `persisted_query`. * **The version integer**, and what a future value would mean. * **The algorithm and its encoding** — SHA-256, hexadecimal, lowercase. * **Exactly which text was hashed.** The digest is taken over a byte string, so the client's copy of the document and the copy it later uploads must be character-identical. Adding a single trailing newline to the document above changes the digest from `fc7cd86c…` to `28ce4466…` — a completely different key. * **The signal for a hash the server has never seen.** Conventionally an entry in the response's `errors` array whose `message` is the literal string `PersistedQueryNotFound`; the client recognises that exact string and reacts to it. Servers that implement the convention but have it switched off conventionally answer with a not-supported message instead. None of this is validated by the schema. `extensions` is not part of the type system: it is not a field, it is not an argument, it takes no directive, and it never appears in introspection. A server is free to ignore any extension entry it does not understand — which is precisely what a server without persisted-query support does with this one. ## Why "ignored" is the sharp edge Silently ignoring an unknown extension is the right default for extensibility, but it interacts badly with this particular convention, because a hash-first request deliberately omits the `query` parameter — the hash is meant to *replace* the document. Send that request to a server that has never heard of the convention and two things happen in order: the extension is discarded as unknown, and the request is then found to have no document at all. The client gets back a request error complaining about a missing query, which is a true statement about the request and a completely misleading one about the cause. Recognising that failure mode — "my hash-only requests all say there is no query" means "the server does not implement the convention", not "my hash is wrong" — is the practical payoff of knowing this is unspecified. ## Unspecified is not unusual It would be easy to hear "convention" as "fringe". It is not. Most mainstream servers implement this one, and a good deal of what people casually call "GraphQL" sits in the same category: connection-and-cursor pagination and global object identification are server conventions; per-field cache hints are a server convention; static cost analysis is a convention; even the composition directives of a federated graph come from a separate specification, not the GraphQL one. The specification itself is deliberately narrow — it defines a language, a type system and an execution algorithm, and says almost nothing about transport, caching or operational concerns. Everything operational grew up around it as convention. What the distinction buys you is a habit: when a question turns on one of these, you answer "where is this defined?" before you answer "how does it work?", because the two answers have different consequences. Specified behaviour is portable and you may rely on it. Conventional behaviour is portable *in practice*, subject to dialect drift, and the authority for "will this work against that server" is that server's documentation rather than the specification. ## Traps The common wrong answers are asserting that the specification defines persisted queries; believing that `extensions` is validated against the schema like a field or an argument; and thinking the server issues the hash to the client. It does not — the client computes the digest of its own document, and the server only ever verifies it.

  • If nothing specifies the extension, how do two independently written implementations interoperate at all?
    By copying the same shape. Both ends hard-code the entry name `persistedQuery`, the version integer `1`, SHA-256 in lowercase hex, and the literal not-found message the client watches for. There is no handshake and no capability advertisement, so agreement is by imitation rather than negotiation — which is also why a mismatch shows up as a confusing runtime failure rather than a clean rejection.
  • What does a server that has never heard of the convention do with a hash-only request?
    It ignores the unrecognised extension entry — the correct default for an implementation-defined map — and then finds the request has no `query` parameter, because the hash was meant to replace it. The client receives a request error saying there is no document. The message is accurate about the request and misleading about the cause, so read it as "this server does not implement the convention" rather than "my hash is wrong".
  • Does the schema constrain what may appear in extensions?
    No. `extensions` is outside the type system entirely: it is not a field, not an argument, carries no directive, and never appears in introspection. Nothing validates its contents, which is exactly what makes it usable as an escape hatch for conventions like this one — and exactly why an unrecognised entry passes silently instead of producing an error a client could act on.

saying these in an interview costs you the question

  • Claims the GraphQL specification defines persisted queries
  • Thinks extensions is validated against the schema
  • Says the server issues the hash to the client
  • Assumes every GraphQL server accepts a hash-only request
  • Believes the hash algorithm is negotiated per request
  • Calls extensions the place variables are carried

context