skip to content

questions

3

In Automatic Persisted Queries, what does the client send first and what happens on a miss?

level: middleimportance: must knowfreq 58%

answer

  1. Send the fingerprint, keep the original
  2. Learned from traffic, not published ahead
  3. A refusal that tells you how to fix it
  4. The retry carries both, not just the text
  5. Two trips cold, one short trip warm

basics

~20 s

The client sends only its document's SHA-256 hash, no query text. A server that knows the hash executes it; a server that does not returns a not-found error, and the client re-sends with the document text, which the server stores and runs.

solid answer

~50 s

The first request carries `operationName`, `variables` and an `extensions.persistedQuery` entry holding `version: 1` and the document's SHA-256 hash — and deliberately **no `query` field**. The server looks the hash up in its store of known documents. On a hit it executes normally and the client has sent a request a fraction of the usual size. On a miss it executes nothing and answers with an entry in `errors` whose message is conventionally `PersistedQueryNotFound`. The client watches for that message and retries the *same* request with the full document text added alongside the unchanged extension. The server hashes the text it received, checks it against the claimed hash, stores the pair, executes, and returns the result. So a cold document costs two round trips and a warm one costs a short single trip. Variables are never part of the hash, so one hash serves every variable set.

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 the two-request shape: a hash with no document, and on an unknown hash a second request that carries the document so the server can learn it. Know that after that first exchange every call is one short request.

for a middle

Explain each step and why the retry must carry both the text and the extension, that only the document is hashed so one hash serves every variable set, and that the digest is over exact bytes so whitespace changes the key.

for a senior

Demonstrate that you can reason about where the failure surfaces — a GraphQL error entry, not a transport error — and what verification on upload protects, plus what a client that never gets a hit is likely doing wrong.

for a principal

Own the adoption argument: this scheme needs no build step and no publication step, so it costs nothing in the release process, and the tradeoff you are accepting is a learned server-side store that can be lost rather than a guaranteed one.

## The handshake, step by step Automatic Persisted Queries trades a long request body for a short one by sending a **fingerprint of the document instead of the document**. "Automatic" is the load-bearing word: there is no build step and no publication step. The server learns the documents it will later recognise from the traffic itself, at runtime, the first time it sees each one. Take an operation on a hospital appointment graph: ```graphql query ClinicDaySlots($clinicId: ID!, $date: Date!) { clinic(id: $clinicId) { name slots(date: $date) { startsAt clinician { displayName } status } } } ``` That is 184 bytes of text the client would otherwise send on every single call. Its SHA-256 digest is `fc7cd86c2ce9a1a1004bb386539671e7bd77d84114ee78113d809324ee47b77a` — 64 characters, fixed, whatever the document's size. **Step 1 — the hash-first request.** The client sends its normal `operationName` and `variables`, plus the extension entry, and omits `query` entirely. This is the request it will send on every subsequent call, forever, and it is the whole point of the exercise. **Step 2 — the lookup.** The server treats the hash as a key into a store of documents it has previously been shown. A hit means it already holds the text; it parses (or reuses a parsed form), validates, executes, and answers normally. Nothing about the response distinguishes it from an ordinary call. **Step 3 — the miss.** If the hash is unknown, the server has nothing to execute. It cannot guess the document and it does not fail the request in a way the client cannot recover from. Instead it answers with a GraphQL error entry whose `message` is conventionally the literal string `PersistedQueryNotFound`, and executes nothing. Note where this failure lives: it is an entry in the response `errors` array, not a transport failure. Client code recognises the exact message string. **Step 4 — the upload.** On seeing that message the client re-sends immediately, and this is the part candidates most often get wrong: the retry carries **both** the full `query` text **and** the same unchanged `persistedQuery` extension. The extension is what tells the server "register this document under this hash", not merely "run this". A retry that dropped the extension would be an ordinary request — it would execute correctly, return the right answer, and store nothing, so the next hash-first call would miss all over again. **Step 5 — verify, store, execute.** The server hashes the text it just received and compares it to the claimed hash. If they match it stores the pair and executes, returning a normal result. If they do not match it refuses to store, because the whole scheme depends on a hash meaning one and only one document. The client gets its answer on this second trip, so a user sees one slightly slower request, not an error. From then on, every call for that operation is one short request. ## What is hashed, and what is not Only the document text is hashed. `operationName` and `variables` travel as ordinary request parameters on both trips and take no part in the digest. That is what makes the scheme economical: one entry in the server's store serves every clinic and every date the operation is ever called with. It is also why the store stays small — its size tracks the number of *distinct operation texts a client ships*, typically dozens, not the number of requests. The flip side is that the digest is over an exact byte string. The document above hashes to `fc7cd86c…`; add a single trailing newline and it hashes to `28ce4466…`. Two texts that differ only in whitespace or field order are, to this mechanism, two entirely unrelated documents. The client must therefore hash and upload the *same* string — a rule that is trivially satisfied when both come from one serialization, and quietly violated when the hash is computed over one representation of the document and a different representation is sent later. ## Why the miss is designed to be recoverable It is worth noticing what the protocol declines to do on a miss. It does not fail the user's request. It does not require the client to pre-register anything. It does not ask an operator to publish a manifest. It converts an unknown fingerprint into a single extra round trip and then heals itself — which is the entire meaning of "automatic", and the reason this is usually the cheapest persisted-document scheme to adopt: turn it on at both ends and nothing else in the release process changes. The price is paid in that extra round trip, and in the fact that the server's knowledge is *learned*, so it can be lost. What that costs in a running system is the operational half of the topic. ## Traps Saying the server returns the result anyway on a miss; saying the client sends the hash and the document together on every call (that would defeat the purpose entirely — it sends both only on the retry); believing variables are folded into the hash; treating the not-found signal as an HTTP-level failure rather than a GraphQL error entry; and imagining the server generates the hash and hands it to the client, when the client computes the digest of its own document and the server only ever verifies it.

  • Why must the retry carry the extension as well as the document text?
    Because the extension is the instruction to register. The server needs a hash to file the document under, and it will only store the pair when the claimed hash matches the text it received. A retry with `query` alone is an ordinary request: it executes and returns the right answer, but the server learns nothing, so the next hash-first call misses again and the client loops between the two forms forever.
  • Are variables part of the hash?
    No. Only the document text is digested; `variables` and `operationName` travel as ordinary request parameters on both trips. That is what keeps the server's store small — one entry serves every variable set the operation is ever called with — and it is why a client that interpolates values into the document text instead of passing variables produces a fresh hash per call and gets no benefit at all.
  • What does the server do if the uploaded document does not hash to the claimed value?
    It refuses to store the pair and answers with an error rather than filing the text under a hash it does not match. The scheme only works if a hash denotes exactly one document, so verification is not optional. A client that repeatedly trips this is usually hashing one representation of the document and uploading a different one.
  • Does the not-found signal arrive as a transport failure or a GraphQL error?
    As a GraphQL error: an entry in the response `errors` array whose message is conventionally the literal string `PersistedQueryNotFound`. The client matches that string to decide to retry. Since the message string is a convention rather than specified behaviour, a client and server pairing that disagrees on it will simply never trigger the retry, and the client will see an unexplained error instead.

saying these in an interview costs you the question

  • Says the server returns the result anyway on a miss
  • Claims hash and document are sent together every time
  • Believes variables are folded into the hash
  • Treats the not-found signal as a transport-level failure
  • Says the server generates and hands out the hash
  • Thinks the retry goes to a different endpoint

context

open as a page

Which Automatic Persisted Queries requests pay two round trips, and why does that keep recurring?

level: seniorimportance: should knowfreq 46%

basics

~20 s

One request pays two trips per document per document-store — and the store is usually per server process, so the cost repeats on every replica, every deploy, every restart and every scale-out. Misses are a steady rate, not a one-off warm-up.

open as a page

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

level: juniorimportance: nice to knowfreq 24%

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.

open as a page