ddtcorex

07 Oct 2026 · 4 min read

Caching GraphQL: full-page cache, query hashing, and cache tags

How CACHABLE_QUERY hashes, cache tags, and identity headers make Magento 2 GraphQL responses cacheable without serving stale storefront data

Part 4 of the API Platform series — cache the answer, invalidate by tag.

GraphQL caching in Magento 2 fights the HTTP layer by default. Responses arrive via POST /graphql, and full-page cache was designed around cacheable GET routes, so a naive deployment executes every storefront query against PHP on every page view. Three mechanisms fix this: marking queries cacheable so the dispatcher can store them, hashing normalized queries so equivalent requests share entries, and tagging entries with entity identities so product saves invalidate exactly what changed. Get all three right and repeat listing and PDP queries serve from cache. Get any one wrong and you serve either nothing cached or, worse, stale prices to the wrong customer.

Why POST responses miss the cache

Varnish and the built-in full-page cache key on method, URL, and vary headers. Two POST /graphql requests to the same URL with different bodies are indistinguishable to a naive cache key, so the safe default is to bypass: pass POSTs straight to the backend. Magento's GraphQL cache support works around this by moving the discriminating information (which query, which variables, which customer context) into cache identifiers the FPC layer understands, but only for queries the module explicitly marks cacheable. Mutations, authenticated customer queries, and cart queries stay uncached by design.

Step 1: mark queries cacheable

A query resolver opts into caching by implementing Magento\Framework\GraphQl\Query\Resolver\CacheableInterface and returning true from isCacheable(). The rule is strict: only mark pure catalog and CMS reads cacheable (product listings, category trees, CMS blocks, store config). Anything shaped by the viewer, cart contents, customer group pricing with session-only context, or mutations must return false.

class ProductListing implements ResolverInterface, CacheableInterface
{
    public function isCacheable(): bool
    {
        return true; // no viewer state, no cart state
    }
}

The failure mode here is over-marking. A resolver that returns true while embedding customer-specific prices poisons the cache with the first viewer's prices for every later viewer. When in doubt, leave a field uncached and measure: an uncached correct field costs latency, a cached wrong field costs trust. Customer-group pricing deserves special care: it is cacheable only when the group identity participates in the cache key via the X-Magento-Vary / customer context headers, which the next section covers.

Step 2: CACHABLE_QUERY hashes

For cacheable operations, Magento computes a stable hash over the normalized query text plus variables, exposed internally as the CACHABLE_QUERY hash. Normalization matters: two clients sending the same fields in different whitespace or field order must map to one cache entry, or the hit rate collapses under real frontend diversity. Persisted or hashed-query setups (the client sends a hash the server maps to a stored document) make this even stronger by removing body variance entirely.

As an integrator, your leverage is query discipline on the frontend: keep the storefront's listing and PDP queries byte-stable (checked-in documents, fixed field order, variables for page/size/filter rather than string interpolation), so the hash space stays small and hot. Ten canonical queries with variable payloads produce ten cache entries. Ten templates interpolated per render produce thousands. Review the access log for /graphql cache-hit headers after a release; a new frontend build that silently rewrites query text shows up as a hit-rate cliff on identical traffic.

Step 3: cache tags and identity headers

Cached entries carry tags (cat_p_123 for product 123, cat_c_45 for category 45, cms_b_footer for blocks), and entity saves emit invalidation by tag. Your custom resolvers must declare the tags their data depends on via getCacheTags() (or the cache-identity interface your version provides), or product edits will leave stale custom fields in cached responses indefinitely. Every cacheable custom field needs a tag function mapping its arguments and result back to entity IDs; a field that cannot name its dependencies is a field that should not be cached.

Identity headers complete the picture. X-Magento-Cache-Id, X-Magento-Vary, and the customer-context / authorization headers partition the cache by viewer state so guest and logged-in responses, or different customer groups, never share entries. Verify with two curl passes: same query, one anonymous and one with Authorization: Bearer <token>, must produce distinct cache entries (distinct X-Magento-Cache-Id values in debug headers). If they collide, a context header is missing and personalization leaks across viewers.

The invalidation checklist

Before calling GraphQL caching done on a project, walk this list on staging:

  1. Tag coverage. Edit a product touched by each custom cacheable field (price, name, custom attribute) and confirm the storefront query re-executes (cache miss) exactly once, then hits again. No miss means missing tags; permanent misses mean over-broad tags.
  2. Mutation safety. Run every cart and checkout mutation and confirm their responses carry no-cache headers. A cached addProductsToCart response replays one shopper's cart to the next.
  3. Vary partitioning. Request the same listing as guest, logged-in customer, and a second customer group; confirm three cache entries, not one.
  4. Hit-rate baseline. Record hit rate for the top five queries under steady traffic. Re-check after each frontend release; query-text drift is the commonest silent regression.

Caching answers how often the backend computes. The remaining problem in this series is writes: cart and checkout mutations, where idempotency, inventory holds, and payment sequencing decide whether headless checkout is reliable. That is Part 5.

Next in this series:

Continue with magento2-graphql-cart-mutations.

magento2graphqlperformance