GraphQL Persisted Queries: How to Slash API Overhead, Lock Down Your Graph, and Scale to Millions of Requests Without Flinching
GraphQL persisted queries are the hidden performance and security lever most engineering teams never pull — this deep-dive shows you exactly how to implement them in production, cut payload sizes by up to 90%, and harden your API surface in one architectural move.
TL;DR / Quick Answer: GraphQL persisted queries replace full query strings with short, pre-registered hash IDs on every network request. The result: up to 90% smaller request payloads, zero ability for clients to run arbitrary queries, dramatically better CDN cacheability, and measurably lower server CPU — all without changing a single line of your resolver logic.
Why GraphQL Persisted Queries Deserve a First-Class Seat in Your Architecture
Every production GraphQL API eventually hits the same trio of problems: bloated request payloads hammering mobile clients on 4G, open query surfaces that let anyone send a deeply nested, resource-exhausting operation, and a CDN that can't cache POST bodies efficiently. GraphQL persisted queries solve all three in a single architectural pattern. At Apargo, we've deployed this pattern across multiple high-traffic SaaS products and shaved average request sizes from ~4 KB down to under 200 bytes — a 95% reduction — while simultaneously eliminating entire classes of injection and DoS risk. This article walks you through the full production implementation: theory, hashing strategy, server-side registry, client integration, CDN caching, and the security model that makes it bulletproof.
What Are GraphQL Persisted Queries, Exactly?
In a standard GraphQL setup, the client sends the full query document on every request:
// Standard GraphQL POST body — ~1.8 KB for a real-world query
{
"query": "query GetUserDashboard($userId: ID!) { user(id: $userId) { id name email profile { avatarUrl bio } orders(last: 10) { id total status createdAt items { productId name quantity } } notifications(unread: true) { id message createdAt } } }",
"variables": { "userId": "usr_9f3a2b" }
}
With GraphQL persisted queries, that entire query string is replaced by a short hash that was pre-registered during your build pipeline:
// Persisted query POST body — ~120 bytes
{
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "a1b2c3d4e5f6..."
}
},
"variables": { "userId": "usr_9f3a2b" }
}
The server looks up the hash in its registry, retrieves the full query string, executes it, and returns the response. If the hash is unknown, the server returns a specific error code that triggers the client to re-send the full query for registration — a flow known as Automatic Persisted Queries (APQ).
The Two Flavors: APQ vs. Locked Persisted Queries
1. Automatic Persisted Queries (APQ)
APQ is the progressive-enhancement version popularized by Apollo Server's APQ documentation. The client optimistically sends only the hash. On a cache miss, the server responds with PERSISTED_QUERY_NOT_FOUND, the client retries with the full query, and the server caches it. From the second request onward, only the hash is needed. This works transparently with zero build-step changes.
- Pros: Zero friction to adopt, no build pipeline required, immediate payload reduction on warm cache hits.
- Cons: Arbitrary queries can still be registered on first contact — the API surface is not locked.
2. Locked (Allow-List) Persisted Queries
The production-grade, security-first version. You extract all queries from your client codebase at build time, generate their hashes, upload the manifest to the server, and then configure the server to reject any query not in the manifest. Unknown hashes get a hard 403 Forbidden. No new queries can be registered at runtime.
- Pros: Complete API surface lockdown, eliminates arbitrary query attacks, enables aggressive CDN caching.
- Cons: Requires a coordinated build/deploy pipeline between client and server.
Apargo's recommendation: Start with APQ in staging to validate the flow. Ship locked persisted queries to production. The security gain is non-negotiable for any customer-facing SaaS.
Building the Production Pipeline: Step-by-Step
Step 1: Extract Queries at Build Time
Using @graphql-codegen with the client-preset, you can automatically extract every gql tagged template literal from your frontend codebase and generate a manifest JSON file.
# Install the necessary tooling
npm install --save-dev @graphql-codegen/cli @graphql-codegen/client-preset \
@graphql-codegen/persisted-documents
# codegen.yml — GraphQL Codegen configuration
schema: "http://localhost:4000/graphql"
documents: "src/**/*.{ts,tsx,graphql}"
generates:
./src/gql/:
preset: client
config:
# Emit persisted documents alongside generated types
persistedDocuments: true
./persisted-query-manifest.json:
plugins:
- persisted-documents
config:
# Use SHA-256 for hashing — required by Apollo APQ spec
hashAlgorithm: sha256
# Output format: {Related Articles
Explore more insights from our engineering and product teams.
