Debug GraphQL Persisted Query Misses
Resolve persisted-query-not-found errors by checking the APQ hash registry and hash generation between client and server.
Task
Trace automatic persisted query misses by comparing client hashes with the server registry and fixing cache warm-up.
When to use this
Clients intermittently get PersistedQueryNotFound after a deploy, forcing a full query upload and extra latency.
Prerequisites
- A GraphQL client using automatic persisted queries
- Server APQ registry access
- The same query documents the client sends
Steps
1. Reproduce the miss and read the extended error code.
curl -s -X POST https://api.example.com/graphql \
-H 'content-type: application/json' \
-d '{"extensions":{"persistedQuery":{"version":1,"sha256Hash":"deadbeef"}},"operationName":"Feed"}' | jq .errorsExpected output: The response carries PersistedQueryNotFound with the hash the server could not resolve.
2. Verify the client hash matches what the server expects.
import { createHash } from 'node:crypto';
const hash = createHash('sha256').update(queryString).digest('hex');
console.log(hash, hash === expectedHash);Expected output: A mismatch reveals whitespace or ordering differences between the client query text and the server registry source.
Troubleshooting
Problem
Only new deploys fail
Solution
Warm the server registry during rollout or let clients upload once, then persist.
Problem
Hash differs by formatting
Solution
Hash the exact operation text and normalise trailing whitespace consistently on both sides.
Limitations
- Registry must be shared across all server instances.
- Does not fix queries the client never sends.
- Hash stability depends on exact formatting rules.
What this recipe does
Trace automatic persisted query misses by comparing client hashes with the server registry and fixing cache warm-up.
Clients intermittently get PersistedQueryNotFound after a deploy, forcing a full query upload and extra latency.
Prerequisites
- A GraphQL client using automatic persisted queries
- Server APQ registry access
- The same query documents the client sends
Procedure
- Reproduce the miss and read the extended error code.
curl -s -X POST https://api.example.com/graphql \
-H 'content-type: application/json' \
-d '{"extensions":{"persistedQuery":{"version":1,"sha256Hash":"deadbeef"}},"operationName":"Feed"}' | jq .errors
Expected result: The response carries PersistedQueryNotFound with the hash the server could not resolve.
- Verify the client hash matches what the server expects.
import { createHash } from 'node:crypto';
const hash = createHash('sha256').update(queryString).digest('hex');
console.log(hash, hash === expectedHash);
Expected result: A mismatch reveals whitespace or ordering differences between the client query text and the server registry source.
How to confirm it worked
Run the commands above and confirm every expected result in order. If a step does not produce the stated output, use the troubleshooting notes before continuing. A passing run shows the client hash exists in the server registry and PersistedQueryNotFound stops appearing after warm-up.
Troubleshooting
Only new deploys fail — Warm the server registry during rollout or let clients upload once, then persist.
Hash differs by formatting — Hash the exact operation text and normalise trailing whitespace consistently on both sides.
Limitations
- Registry must be shared across all server instances.
- Does not fix queries the client never sends.
- Hash stability depends on exact formatting rules.