Choosing Between Persisted Query Hashing and Dynamic Queries in Apollo Server
Decide between persisted query hashing and dynamic queries in Apollo Server to optimize for latency, security, and developer velocity.
19 Apr 2026, 14:19 UTC

The Problem: Parsing Overhead and Security Risks
In a standard GraphQL setup, clients send full query strings to the server. The server must parse and validate these strings against the schema on every single request. For high-traffic applications, this creates two primary issues: significant CPU overhead during the parsing phase and a security vulnerability where malicious actors can send deeply nested or complex queries to exhaust server resources (DoS attacks).
The takeaway: Use persisted query hashing when you need to minimize request latency and lock down the server to a known set of approved queries. Stick with dynamic schema-first queries during early development or for internal tools where flexibility outweighs performance.
Decision Constraints
- Latency: If your API serves mobile clients on slow networks, reducing request payload size is critical.
- Security: If the API is public-facing, you need to prevent arbitrary query execution.
- CI/CD Pipeline: Persisted queries require a build step to generate hashes; your pipeline must support this synchronization.
- Memory Budget: The server must be able to store a mapping of hashes to query strings in memory or a cache.
Comparison of Implementation Options
| Metric | Persisted Query Hashing | Dynamic Schema-First Queries |
|---|---|---|
| Payload Size | Minimal (Hash only) | Large (Full query string) |
| Server CPU | Low (Map lookup) | High (Parse & Validate) |
| Attack Surface | Low (Only known hashes execute) | High (Any valid schema query) |
| Dev Velocity | Slower (Requires hash sync) | Faster (Instant iteration) |
| Operational Risk | Cache misses/mismatched hashes | Resource exhaustion (DoS) |
Trade-offs and Engineering Decisions
Persisted queries shift the complexity from the runtime to the build time. By replacing a dynamic string with a hash (e.g., SHA-256), the server skips the expensive lexing and parsing stages. However, this introduces a versioning dependency: if the client sends a hash the server doesn't recognize, the request fails unless a fallback mechanism (like automatic query upload) is enabled.
Dynamic queries provide maximum flexibility. Developers can test new fields in a GraphQL playground without redeploying the client. This is ideal for rapid prototyping but dangerous for production environments where an unoptimized query could crash a database.
Implementation Guide (Apollo Server)
This implementation assumes Apollo Server v3+ and a Node.js environment. You will need administrative access to the server configuration and the client-side build process.
1. Server-Side Configuration
Install and enable the persisted queries plugin. For production, replace the default in-memory map with a durable store like Redis to prevent cache loss during restarts.
const { ApolloServer } = require('apollo-server');
const { persistedQueries } = require('apollo-server-plugin-persisted-queries');
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
persistedQueries({
ttl: 3600, // Cache entries for 1 hour
maxSize: 5000 // Limit memory usage to 5k queries
})
]
});
server.listen({ port: 4000 }).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
2. Client-Side Integration
Configure the Apollo Client to use the PersistedQueries middleware. This ensures the client sends the hash first and only uploads the full query string if the server returns a "PersistedQueryNotFound" error.
import { ApolloClient, InMemoryCache } from '@apollo/client';
import { PersistedQueries } from 'apollo-server-plugin-persisted-queries';
const client = new ApolloClient({
uri: 'http://localhost:4000',
cache: new InMemoryCache(),
middleware: [PersistedQueries()],
});
Verification and Diagnostics
To confirm the implementation is working as intended, perform the following checks:
- Log Inspection: Monitor server logs for
persisted_query_hit: true. If you seepersisted_query_missconsistently, the client is failing to upload hashes or the TTL is too short. - Network Analysis: Open the browser DevTools Network tab. A successful persisted query request should show a small payload containing a
extensionsfield with the hash, rather than a largequerystring. - Load Testing: Use a tool like
k6orautocannonto send 1,000 requests. Compare the CPU utilization of the server when using hashes versus full strings. You should observe a measurable drop in CPU usage per request.
Limitations and Rollback
Limitations:
- Cold Starts: If using an in-memory cache, a server restart will cause a spike in latency as clients re-upload their queries.
- Hash Collisions: While rare with SHA-256, collisions can theoretically occur.
Rollback: To revert to dynamic queries, remove the persistedQueries plugin from the Apollo Server configuration and remove the PersistedQueries middleware from the client. This restores the standard behavior of sending full query strings.