persisted-query-strategy-reference
Pure-reference catalog of GraphQL Persisted Query strategies: the Apollo APQ SHA-256 hash protocol, the PersistedQueryNotFoundError retry flow, the `extensions.persistedQuery` payload, GET-vs-POST and CDN-cache implications, and the three operation modes (auto-register, strict allowlist, hybrid). Use to design or audit a server's persisted-query request layer. This strategy reference emits no tests; to author the runtime tests use apollo-server-tests, graphql-yoga-tests, or mercurius-tests, and for introspection lockdown see introspection-attack-surface-reference.
Install with skills.sh (any agent)
npx skills add testland/qa --skill persisted-query-strategy-referencepersisted-query-strategy-reference
Overview
Pure-reference catalog of GraphQL persisted-query strategies. Per Apollo Server docs (apollographql.com/docs/apollo-server/performance/apq (opens in new window)): "A persisted query is a query string that's cached on the server side, along with its unique identifier (always its SHA-256 hash)."
Two motivations, often conflated:
The configuration mode determines which motivation dominates. Consumed by per-framework test authors.
When to use
The hash + extensions protocol
Per Apollo Server docs, the request format:
GET /graphql
?extensions={"persistedQuery":{"version":1,"sha256Hash":"<HEX>"}}
&variables={"id":"u1"}Or as POST:
{
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"
}
},
"variables": { "id": "u1" }
}Apollo's example: { __typename } hashes to ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38.
The three modes
Mode 1 - APQ auto-register (default, permissive)
const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: {
ttl: 900, // 15 minutes
},
});Flow on first request with new hash:
What this gets you:
Best for: performance optimisation; not a security control.
Mode 2 - Persisted-query-only (allowlist, strict)
const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: false, // Apollo's auto-APQ off
});
// Externally: build a manifest of allowed hashes during CI,
// load into a `pre-registered` store, reject anything not in it.Architecturally: the persisted-queries store is pre-populated during the build/deploy (via codegen of the client app), not by client requests. Any unknown hash → reject (not register-then-execute).
Flow:
What this gets you:
Best for: internet-facing production APIs with a known client app (web / mobile).
Tradeoff: breaks ad-hoc clients (admin tools, GraphiQL, internal Postman collections). Mitigate via a separate admin-only endpoint, or a long-lived admin token that bypasses.
Mode 3 - Hybrid (allowlist prod, auto-register dev)
const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries:
process.env.NODE_ENV === 'production'
? false // Mode 2 setup elsewhere
: { ttl: 900 }, // Mode 1 for dev
});Best of both: dev gets the iteration speed of auto-register; prod gets the allowlist. Risk: config drift - staging may use dev settings.
Implementation patterns
Client side (Apollo Client)
Per Apollo docs:
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
const link = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true, // CDN-cacheable GETs
}).concat(new HttpLink({ uri: '/graphql' }));
const client = new ApolloClient({ link, cache: new InMemoryCache() });useGETForHashedQueries is the CDN-cache lever - without it, POST requests aren't cacheable by most CDNs.
Generating a manifest for strict mode
npx graphql-codegen --config codegen.yml
# Outputs operation strings + hashes to manifest.json
# Per Apollo, with @apollo/persisted-query-lists for build-time
# generation:
npm run extract-queries -- --output manifest.jsonThen in CI / deploy: upload manifest.json as a JSON artifact; server boot reads it.
Disable APQ entirely (no persisted queries)
Per Apollo: persistedQueries: false.
Per-framework support
| Framework | Persisted-query support |
|---|---|
| Apollo Server | Built-in: persistedQueries: { ttl } or false |
| GraphQL Yoga | @graphql-yoga/plugin-persisted-operations (per yoga docs) |
| Mercurius | cache: { ... } + custom resolver |
| Hasura | Allow lists via query_collections + add_to_allowlist mutations |
| Pothos | Configure via underlying server |
Testable behaviours
| Mode | Test |
|---|---|
| Mode 1 | First request with unknown hash → 200 with PERSISTED_QUERY_NOT_FOUND extension; retry succeeds |
| Mode 2 | Request with unregistered hash → 400 with no registration; subsequent requests still 400 |
| Mode 2 | Request with raw query field → 400 (strict mode rejects unhashed) |
| Mode 3 | Same as Mode 1 in dev; same as Mode 2 in prod (test against both NODE_ENV) |
| All modes | Manifest reload preserves existing hashes (no flush during deploy) |
| All modes | TTL expiration in Mode 1 → fallback to retry flow (must not 500) |
These tests prove the chosen mode is actually in effect.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Mode 1 with introspection disabled, expecting allowlist | Auto-register accepts any query; allowlist isn't real | Mode 2 (build-time manifest) is the only true allowlist |
| Strict mode without a manifest workflow | First deploy = total service outage (no queries allowed) | Manifest extraction in client build, upload before server deploy |
| Manifest as a single file in image | Schema changes require full image rebuild | Externalise to S3 / config service; reload on signal |
| GraphiQL exposed alongside strict APQ | GraphiQL queries fail; team disables APQ to "debug" | Separate admin endpoint for GraphiQL, with explicit auth |
| TTL too short (60s) | Cold-cache misses → high PERSISTED_QUERY_NOT_FOUND rate | ttl: 900 (15min) or longer for stable queries |
| TTL too long (forever) | Old query versions stick around; security policy stale | Refresh on deploy; ttl 1-7 days max |
No test asserting PERSISTED_QUERY_NOT_FOUND flow works | Client retry logic silently breaks; perf regression unnoticed | E2E test: drop cache, send only-hash, assert retry flow |
| CDN caches POSTs of hashed queries by mistake | Tenant-id in variables → cross-tenant cache contamination | useGETForHashedQueries: true + per-tenant cache key derivation |
Limitations
References
Related skills
apollo-server-tests
Wraps Apollo Server testing patterns: `server.executeOperation()` (in-process, no HTTP), `supertest` against an ephemeral-port HTTP server (port 0), context injection via the `contextValue` second-argument, and assertion patterns for response shape + errors. Includes the production-config gates testable through this skill - introspection-disabled, persisted-query mode, hideSchemaDetailsFromClientErrors. Use when writing tests for an Apollo Server v4+ GraphQL service.
graphql-complexity-limit-tester
Crafts over-limit depth and complexity queries then asserts rejection before execution, verifying that graphql-depth-limit, graphql-cost-analysis, and graphql-armor (max-depth / cost-limit / max-tokens plugins) are actually enforced and not just configured. Use when auditing a GraphQL service for DoS exposure after depth or cost limits have been added as mitigations, or when adding tests that prove the limits in CI before a production deployment.
graphql-n-plus-one-remediation
Traces a GraphQL resolver tree to locate the N+1 pattern (one parent query returns N rows, then a child field resolver fires once per row), classifies every child field resolver as safe or N+1 risk, and applies one of three fixes: per-request DataLoader batching, eager projection in the parent resolver, or selection-set-aware prefetch. Use when a list-returning resolver is added or changed in review, when a connection-pool exhaustion or slow-query alert traces back to GraphQL traffic, or when a resolver trace shows a child field resolved once per parent row.
graphql-subscription-test-author
Authors GraphQL subscription resolver test suites over graphql-ws (WebSocket) and graphql-sse (Server-Sent Events) transports: subscribe to event streams via the async-iterator API, assert emitted data shape and sequence, verify connection lifecycle and protocol close codes, and validate auth-on-connect (connectionParams / authenticate callback) plus resolver-level pubsub trigger logic. Use for real-time subscription operations; not for queries or mutations - for those use apollo-server-tests, graphql-yoga-tests, or mercurius-tests.
graphql-yoga-tests
Tests a GraphQL Yoga server (the-guild.dev runtime) with `yoga.fetch()` for in-process, no-network request simulation of queries and mutations, `@graphql-tools/executor-http` for subscription and incremental-delivery (streaming) tests, auth-header pass-through, and production-config gates for disabled introspection and persisted operations. Use to test a GraphQL Yoga server, write Yoga query, mutation, or subscription tests, or check its production plugin config; for a different runtime harness use apollo-server-tests, mercurius-tests, or hasura-tests instead, not this skill.
hasura-tests
Wraps Hasura GraphQL Engine testing patterns: docker-compose test instance, the metadata API for declarative schema/permission setup, x-hasura-role and x-hasura-user-id session headers for role-based permission tests, the v1/graphql endpoint via curl/HTTPie/native HTTP clients, and the role-by-table-by-operation permission-matrix pattern. Use for a metadata-driven Hasura engine where row-level permissions dominate; for a code-first server runtime harness use graphql-yoga-tests, apollo-server-tests, or mercurius-tests instead, not this skill.
introspection-attack-surface-reference
Pure-reference catalog of GraphQL introspection as an attack surface and the production-deployment controls for it. Covers what introspection exposes (every type, field, directive, deprecation, description via __schema / __type), Apollo Server's default behaviour (introspection: false when NODE_ENV=production), the `hideSchemaDetailsFromClientErrors: true` companion setting (strips 'did you mean' suggestions), Yoga / Mercurius / Hasura equivalents, query-depth + query-cost limits, persisted-query allowlisting as the strongest mitigation, and the testable behaviours each control creates. Use when designing the production-safety posture of a GraphQL server or auditing an existing deployment.
mercurius-tests
Wraps Mercurius (Fastify GraphQL plugin) testing patterns: `app.inject()` for HTTP-layer simulation without spinning up a network listener, plugin-registration setup (await app.register(mercurius, { schema, resolvers, graphiql: false })), production-config gates (graphiql: false; jit threshold; query depth limits via fastify-rate-limit + complexity), and the per-test app lifecycle (app.close() in afterEach). Use when writing tests for a Fastify + Mercurius GraphQL server.
pothos-builder-tests
Wraps Pothos GraphQL schema-builder testing patterns: testing the SchemaBuilder output (lexicographicSortSchema + printSchema for snapshot tests), testing resolvers via the standard `graphql()` function from graphql-js (no server needed), integration with Apollo Server / GraphQL Yoga (Pothos emits standard graphql-js schemas), and code-first builder unit tests. Covers the SchemaBuilder API surface (queryType, mutationType, objectType, t.field, t.arg). Use when testing a Pothos-built schema before or alongside the server-runtime tests.