Cut GraphQL Payloads with Persisted Queries in Apollo Client and Server
Learn how Apollo’s persisted‑queries feature shrinks request size, hardens security, and what you need to keep in sync. A step‑by‑step guide, example config, and trade‑offs are included.
09 Aug 2026, 00:45 UTC

The Payload Problem
When a GraphQL operation is sent over the wire, the entire query string is part of the HTTP payload. Large queries, especially those with many nested fields, can balloon the request to several kilobytes. On mobile networks or high‑latency links, that extra data translates directly into slower page loads and higher costs.
In addition to performance, sending raw queries exposes them to potential injection attacks if the server does not strictly validate incoming strings.
What Persisted Queries Do
Persisted queries replace the full query text with a short, deterministic hash. The client sends only the hash; the server looks up the corresponding query in a registry and executes it. The benefits are threefold:
- Payload reduction. A 200‑byte hash can replace a 2 KB query, cutting request size by up to 90%.
- Security. The server validates that the hash exists in its registry, preventing arbitrary query execution and mitigating injection risk.
- Cache compatibility. Apollo’s caching and type policies continue to work because the server still receives the full query text internally.
Enabling Persisted Queries – Client Side
Only a handful of lines are required to wire the PersistedQueryLink into an Apollo Client instance. The link must be inserted before the HttpLink so that the hash is resolved before the request is sent.
import { ApolloClient, InMemoryCache, HttpLink, ApolloLink } from '@apollo/client';
import { PersistedQueryLink } from '@apollo/client/link/persisted-queries';
const httpLink = new HttpLink({ uri: '/graphql' });
const persistedLink = new PersistedQueryLink({ useGETForHashedQueries: true });
const client = new ApolloClient({
link: ApolloLink.from([persistedLink, httpLink]),
cache: new InMemoryCache(),
});
Key points:
useGETForHashedQueriesis optional but useful for debugging – it switches to a GET request when only a hash is present.- All queries must be defined statically so that the hash can be generated at build time.
Enabling Persisted Queries – Server Side
Apollo Server accepts a persistedQueries option. The option can point to a JSON file that maps hashes to query strings. The server will reject any hash that is not present.
import { ApolloServer, gql } from 'apollo-server';
import persistedQueries from './persisted-queries.json';
const typeDefs = gql`
type Query { hello: String }
`;
const resolvers = { Query: { hello: () => 'world' } };
const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries,
});
server.listen().then(({ url }) => console.log(`🚀 Server ready at ${url}`));
The persisted-queries.json file is typically generated by a build step that hashes each query. A minimal example of that file looks like this:
{
"e3b0c44298fc1c149afbf4c8996fb924": "query GetUser($id: ID!) { user(id: $id) { id name } }"
}
Concrete Build & Test Workflow
- Generate the manifest. In a real project you would use a script that walks your source files, extracts every
gqltemplate literal, and writes the hash‑to‑query map. For illustration, assume you have ageneratePersistedQueries.jsthat outputspersisted-queries.json. - Deploy the manifest. Copy
persisted-queries.jsonto the server’s file system and reference it inpersistedQueriesas shown above. - Run the client. Build your React or Node app. When the client sends a query, the network tab in Chrome DevTools should show a payload that contains only the hash, e.g.
{"extensions":{"persistedQuery":{"version":1,"sha256Hash":"e3b0c44298fc1c149afbf4c8996fb924"}}}. - Verify resolution. In Apollo Server logs, you should see the full query string resolved from the hash. Apollo DevTools also displays the hash next to the executed query, confirming the mapping.
Note that you should not rely on console logs alone; add a middleware that logs the resolved query during development to catch mismatches early.
Trade‑offs & Caveats
- Deployment sync. The client build and server deployment must use the same manifest. A mismatch results in
400 Bad RequestwithpersistedQueryNotFound. Automate the CI pipeline to fail if the client’s generated manifest does not match the one deployed. - Debug visibility. When using persisted queries, the raw query is no longer visible in the request body. Enable logging of query IDs during debugging or use Apollo DevTools to inspect the hash.
- Hot‑reload complexity. In a development environment that uses file‑watching and hot‑module replacement, the client might send a hash for a query that has just changed. The server will reject it until the manifest is updated. A simple workaround is to disable persisted queries in dev mode or rebuild the manifest on each change.
- Granularity. Persisted queries are most effective for large, frequently executed operations. Small one‑off queries may not benefit enough to justify the build step.
Actionable Takeaway
Persisted queries are a lightweight, low‑overhead way to shrink GraphQL payloads, harden your API, and keep caching behavior unchanged. To adopt them:
- Add
PersistedQueryLinkto your Apollo Client. - Generate a
persisted-queries.jsonmanifest during your build. - Deploy the manifest alongside your server and reference it via
persistedQueries. - Integrate CI checks that compare the client‑generated manifest with the one deployed.
- Enable query‑ID logging in dev to keep debugging transparent.
With these steps in place, you’ll see a measurable drop in network traffic and an extra layer of protection against unintended query execution.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.