Persisted Queries: Security and Performance in GraphQL
AI generated
{ }
type
GraphQL · Security · Performance · Caching · Magento
Persisted Queries:
Security and Performance in GraphQL

Persisted Queries solve two problems at once: they reduce the amount of data transferred and close a security gap that is often overlooked in public GraphQL APIs. Anyone who accepts arbitrary queries hands attackers a free pass. Anyone who only allows known queries gains control, without sacrificing flexibility entirely.

10 min read APQ · Whitelisting · SHA-256 · CDN Caching · Magento GraphQL · Production · Security

1. The problem with arbitrary queries in public APIs

An open GraphQL API that accepts arbitrary queries is fundamentally risky from a security perspective. An attacker can send queries with extreme depth, a huge number of fields, or circular fragments that block the server for minutes, even when query depth limits and complexity limits are in place. These parameters can never cover every attack vector, because the schema itself defines the complexity, and not every expensive field is automatically recognized as such.

On top of that there is a performance problem: every GraphQL query is transmitted as text over HTTP, validated, parsed, and then executed. For a production app serving thousands of requests per minute, the query text itself is an unnecessary burden. In addition, GET requests with a dynamic query body can barely be cached at the CDN level, because the body cannot serve as a cache key. Persisted Queries solve all three problems at once: they reduce the payload, allow GET caching, and enable whitelisting.

2. What Persisted Queries are and how they work

A Persisted Query is a pre-registered GraphQL query that is retrieved via a unique identifier, typically a SHA-256 hash of the query text. Instead of sending the full query text with every request, the client only sends the hash. The server looks up the matching query in its store, executes it, and returns the result. This considerably reduces the payload for complex queries, because a SHA-256 hash is always 64 characters long, while the query text can span hundreds or thousands of characters.

The store for Persisted Queries can be implemented in different ways: in-memory cache, Redis, database-backed, or an external query registry such as GraphQL Hive. What matters is that the store stays consistent between frontend build time (where queries get registered) and runtime (where queries get resolved). Deployments therefore need to happen in a way that updates the store before the new frontend code goes live, otherwise the server encounters queries with an unknown hash and returns an error.


# This query is registered at build time with its SHA-256 hash
# Client only sends the hash at runtime, not the full query text
query ProductListQuery($search: String!, $pageSize: Int!) {
  products(search: $search, pageSize: $pageSize) {
    total_count
    page_info {
      current_page
      page_size
      total_pages
    }
    items {
      sku
      name
      url_key
      price_range {
        minimum_price {
          final_price { value currency }
        }
      }
    }
  }
}
# SHA-256 hash: 7a3f1c2e9b4d6f8a0e2c4b1d3e5f7a9b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e0

3. Automatic Persisted Queries (APQ): the two-step protocol

Automatic Persisted Queries (APQ) is a protocol developed by Apollo that automates the registration step. On the first request the client sends the SHA-256 hash of the query to the server. If the server already knows the query, it responds directly. If not, it returns a PersistedQueryNotFound error. The client then sends a second request with the hash and the full query text. The server registers the query in its cache and responds. From the next request onward, the hash alone is enough again.

APQ is a pragmatic compromise: it requires no upfront registration and no build-time infrastructure, yet it still delivers the performance benefits for every request after the first one. For security purposes, APQ alone is not sufficient, because the server still accepts arbitrary queries, it just caches the query texts more efficiently. Real whitelisting needs a separate mechanism that does not let the hash store grow with unknown hashes, but only allows pre-registered queries.


# APQ: First request, client sends only the hash
# POST /graphql
# { "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "7a3f1c..." } } }
# Server responds: { "errors": [{ "message": "PersistedQueryNotFound" }] }

# APQ: Second request, client sends hash + full query
# POST /graphql
# {
#   "query": "query ProductListQuery(...) { ... }",
#   "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "7a3f1c..." } }
# }
# Server registers query and responds with data

# APQ: All subsequent requests, only the hash is needed
# GET /graphql?extensions={"persistedQuery":{"version":1,"sha256Hash":"7a3f1c..."}}
# CDN can now cache this GET request by URL
query AnyRegisteredQuery {
  products(search: "jacket", pageSize: 10) {
    total_count
    items { sku name }
  }
}

4. Query whitelisting: only allow known queries

Query whitelisting is the stricter variant: the server maintains a fixed list of known queries (as hashes or full texts), and only these get executed. Unknown hashes and arbitrary query texts are rejected. This closes the attack vector completely: an attacker cannot construct a custom query and submit it, because the server will not execute it. The list is typically generated during the frontend build and rolled into the server store.

The downside is obvious: whitelisting means every new query in the frontend requires a deploy step. For production applications with multiple frontends and frequent releases, that is a real coordination effort. In practice, whitelisting is therefore mostly used for public, unauthenticated APIs, such as catalog queries, homepage data, and similar. For internal admin tools and authenticated areas, the security gain is smaller and the effort is hardly justified.

5. CDN caching with Persisted Queries

One of the most valuable practical benefits of Persisted Queries lies in CDN caching. Standard GraphQL requests are transmitted via POST, and POST requests are not cacheable per the HTTP specification. Persisted Queries with APQ make it possible to transmit queries via GET, the hash and optional variables end up in the URL's query string, and GET responses can easily be cached by CDNs such as Fastly, Cloudflare, or Varnish.

This is especially interesting for publicly accessible, non-user-specific data: product lists, category trees, homepage content, and similar can be served at the CDN edge without putting load on the origin server. The cache key generation does need to be built carefully, however, so that variables, locale, and other relevant parameters are part of the cache key. Broken cache keys mean that users get the wrong data from the CDN cache, a common and hard-to-debug problem.


# GET request with persisted query, CDN-cacheable
# GET /graphql?operationName=HomepageData&extensions={"persistedQuery":{"version":1,"sha256Hash":"abc123"}}&variables={"locale":"de"}
# Cache-Control: public, max-age=300, stale-while-revalidate=60

# Query registered at build time, only its hash is sent at runtime
query HomepageData($locale: String!) {
  categoryList(filters: { parent_id: { eq: "2" } }) {
    id
    name
    url_key
    children_count
  }
  cmsPage(identifier: "homepage") {
    title
    content
    meta_description
  }
}

6. Common implementation mistakes

The most common mistake with Persisted Queries is deployment timing. If the frontend is deployed with new query hashes before the server has the current hash store, every request fails with an error. The fix: always update the hash store before the frontend deploy, and never deploy both at the same time. A transition phase, in which both stores are active, is often necessary for zero-downtime deployments.

Another mistake is forgetting variables in the cache key. A Persisted Query with the hash ID for a product list query returns different results depending on the value of the search variable. CDN caching without variables in the cache key means the first user who searches for "jacket" ends up delivering the jacket search results to the second user who searches for "shirt". That is not just wrong, it is embarrassing. Variables must always be part of the cache key, either in the query string or derived into a cache header.

7. Persisted Queries in Magento GraphQL

Magento does not support Persisted Queries natively out of the box, but the framework it uses allows an implementation via a plugin mechanism or a separate middleware layer. In practice, APQ is often implemented at the level of a GraphQL gateway (e.g. Apollo Router, GraphQL Mesh) in front of Magento, not directly inside Magento itself. That has advantages: the gateway can manage the APQ cache, and Magento always sees complete queries, regardless of whether the client uses APQ or not.

For CDN caching, Magento offers its own caching infrastructure via X-Magento-Cache-Id and Varnish integration. This can be combined with Persisted Queries by deriving the cache key from hash plus variables plus customer group context. Unauthenticated product list queries can then be cached at the CDN edge and served directly, without putting load on Magento. The combination of Persisted Queries, CDN caching, and Magento's own cache tags is one of the most effective performance patterns for Magento headless.

8. With and without Persisted Queries compared

The table below shows the concrete differences between a standard GraphQL API and an API using Persisted Queries across several dimensions.

Dimension Without Persisted Queries With Persisted Queries Note
Request payload Full query text (100 to 5000 characters) SHA-256 hash (64 characters) Up to 98% smaller for large queries
CDN caching Not possible (POST) Possible via GET Variables must be part of the cache key
Query security Arbitrary queries allowed Only whitelisted queries Only with strict whitelisting, not APQ alone
Deploy complexity Simple Timing between frontend and store Store must be current before frontend deploy
Development flexibility Full flexibility Every new query needs registration Whitelisting restricts ad hoc queries

9. Summary

Persisted Queries are not an optional nice-to-have for production GraphQL APIs, they are one of the most effective measures for security and performance. APQ delivers immediately measurable performance improvements through smaller payloads and enables CDN caching via GET requests. Whitelisting closes the attack vector of arbitrary query complexity completely and makes the API substantially more robust for public use cases. The effort mostly lies in build-time integration and deployment timing.

For Magento headless the rule is: the combination of Persisted Queries, CDN caching, and Magento's own cache tags is one of the most effective patterns for taking load off the origin server and minimizing response times for end users. The implementation requires a gateway layer in front of Magento, but it is achievable with tools such as Apollo Router, GraphQL Mesh, or even a simple Nginx layer. The long-term performance gains clearly outweigh the one-time implementation effort.

Persisted Queries, the essentials at a glance

APQ protocol

Two steps: first send only the hash, on failure send hash plus query text. After that, only the hash. No build-time setup needed, but no real whitelisting either.

Whitelisting

Only pre-registered queries get executed. Closes the attack vector of arbitrary queries completely. Requires coordination between frontend build and server deploy.

CDN caching

GET requests with the hash in the query string are CDN-cacheable. Variables and locale must be part of the cache key, otherwise other users get the wrong data.

Magento recommendation

Gateway layer (Apollo Router, GraphQL Mesh) in front of Magento for the APQ cache. CDN caching of unauthenticated queries combined with Magento cache tags.

11. FAQ: Persisted Queries in GraphQL

1What are Persisted Queries?
Pre-registered queries that get called via a SHA-256 hash instead of the full query text. Smaller payload, CDN caching possible, whitelisting achievable.
2APQ vs. whitelisting: which is more secure?
APQ caches automatically, but still allows arbitrary queries. Only whitelisting closes the attack vector completely, by having the server reject unknown hashes.
3Why do Persisted Queries enable CDN caching?
POST is not cacheable. With APQ, queries can be transmitted via GET (hash plus variables in the query string). GET responses can be cached by CDNs.
4What happens on a deployment error?
With APQ: the client automatically sends the full query text as a fallback. With strict whitelisting: the request fails completely. So always update the store before the frontend deploy.
5How do you implement APQ in Magento?
Not natively, implement a gateway layer (Apollo Router, GraphQL Mesh) in front of Magento. The gateway manages the APQ cache; Magento always receives complete queries.
6Why must variables be part of the cache key?
Without variables in the cache key, the CDN serves identical responses for every parameter combination, for example search results for "jacket" to users searching for "shirt". Variables, locale, and customer group must be part of the key.
7How are queries registered during the build?
GraphQL Code Generator or Apollo Client extract all queries from the code, compute hashes, and create a manifest. The manifest gets rolled into the server store before the frontend deploy.
8Does whitelisting close all GraphQL security problems?
No. Whitelisting prevents arbitrary queries, but not abuse of allowed queries by authenticated users. Auth, field-level permissions, rate limiting, and introspection control are still necessary.
9How large is the performance gain?
SHA-256 is always 64 characters; complex queries can be thousands of characters, which corresponds to up to 98% smaller payload. CDN caching of unauthenticated queries is the biggest performance lever.
10Is APQ worthwhile for internal APIs too?
For internal auth APIs, whitelisting is less useful, but APQ is still helpful for smaller payloads and a server-side query-parsing cache. The performance gain is measurable, even without CDN caching.