GraphQL Persisted Queries
Persisted Queries is a technique where the client sends a query hash instead of full body. Server caches queries by hash. Benefits: reduced traffic (especially mobile), enables GET requests for CDN caching, protects against arbitrary queries in production.
How Automatic Persisted Queries (APQ) Works
Client Server CDN/Cache
│ │ │
│ POST {hash} │ │
│───────────────>│ │
│ 404 Not Found │ │
│<───────────────│ │
│ │ │
│ POST {hash + query body} │
│───────────────>│ │
│ {data} [save hash→query] │
│<───────────────│ │
│ │ │
│ GET ?hash=... │ │
│──────────────────────────────> │
│ {data} from cache │
│<────────────────────────────── │
Apollo Client: Setting Up APQ
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries'
import { generatePersistedQueryIdsFromManifest } from '@apollo/persisted-query-lists'
import { sha256 } from 'crypto-hash'
// Approach 1: Automatic Persisted Queries (fallback on cache miss)
const persistedQueryLink = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true // GET requests for CDN caching
})
const httpLink = createHttpLink({
uri: 'https://api.example.com/graphql'
})
const client = new ApolloClient({
link: persistedQueryLink.concat(httpLink),
cache: new InMemoryCache()
})
Server-Side APQ Support
// Apollo Server built-in APQ support
import { ApolloServer } from '@apollo/server'
import { createClient } from 'redis'
import { BaseRedisCache } from 'apollo-server-cache-redis'
const redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()
const server = new ApolloServer({
typeDefs,
resolvers,
// APQ cache: in-memory by default, Redis in production
cache: new BaseRedisCache({
client: redis,
ttl: 86400 // 24 hours
}),
// Enable APQ (enabled by default)
persistedQueries: {
ttl: 86400 // Cache lifetime
}
})
Registered Persisted Queries (More Secure)
APQ allows executing any query after registration. Registered PQ only allows pre-known queries:
# Generate manifest from client operations (at build time)
npx generate-persisted-query-manifest \
--documents "src/**/*.graphql" \
--output persisted-query-manifest.json
// persisted-query-manifest.json
{
"format": "apollo-persisted-query-manifest",
"version": 1,
"operations": [
{
"id": "dc67510fb4289672bea757e862d6b00e...",
"name": "GetPosts",
"type": "query",
"body": "query GetPosts($limit: Int) { posts(first: $limit) { ... } }"
},
{
"id": "e8c72e4d9b1a2f8e5c3d7a9b1f4e8c9d...",
"name": "CreatePost",
"type": "mutation",
"body": "mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { ... } }"
}
]
}
// Server validates against manifest
import { ApolloServer } from '@apollo/server'
const allowedQueries = new Set(
manifest.operations.map(op => op.id)
)
const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: {
ttl: 86400
},
// Only allow registered queries
plugins: [{
async requestDidResolveOperation({ request }) {
const id = request.http?.headers?.get('x-graphql-query-id')
if (id && !allowedQueries.has(id)) {
throw new Error('Persisted query not found')
}
}
}]
})
Benefits
- Reduced bandwidth: 50-70% smaller requests on mobile
- CDN caching: GET requests can be cached by standard CDN
- Query whitelisting: prevents unintended queries in production
- DDoS protection: only known queries execute on server
Timelines
Implementing APQ (automatic approach): 1–2 days. Registered PQ with manifest generation and CI/CD integration: 2–3 days.







