Caching GraphQL

Only GET queries with a cache identity are cached, and the X-Magento-Cache-Id header keeps one shopper's data out of another's.

Syntaxpublic function getIdentities(array $resolvedData): array

Magento stores GraphQL responses in the same full page cache that serves category pages, built-in or Varnish. Three things have to line up, and if any one is missing nothing is cached.

The query must opt in

A query is cacheable only when its schema declaration carries @cache(cacheIdentity: "..."). In 2.4.9 products, categories, cmsPage, cmsBlocks, storeConfig, route, countries and currency do. cart, customer and customerOrders carry @cache(cacheable: false) and are never cached. Caching is also skipped entirely for anything that is not a GET request.

Tags make invalidation possible

A cache tag is a label attached to a cached entry so it can be purged later. The cacheIdentity class implements IdentityInterface, whose getIdentities(array $resolvedData) reads the resolver's own output and returns tags for the rows it contains. Saving one product then invalidates only entries carrying that product's tag. The tags are also sent back on the response in X-Magento-Tags.

One shopper's cache is not another's

Magento returns an X-Magento-Cache-Id header: a hash of the store, currency, logged-in flag, customer group and tax rate, plus a salt held in app/etc/env.php. The client must send that header back on later requests, so a request is only served an entry built for the same context. Add a factor by implementing CacheIdFactorProviderInterface and registering it under idFactorProviders on Magento\GraphQlCache\Model\CacheId\CacheIdCalculator. Apply with bin/magento cache:flush.

Example

Example · php
<?php
// app/code/Learning/Faq/Model/Resolver/FaqEntries/Identity.php
declare(strict_types=1);

namespace Learning\Faq\Model\Resolver\FaqEntries;

use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface;

class Identity implements IdentityInterface
{
    public const CACHE_TAG = 'learning_faq';

    /**
     * Build cache tags from the array the resolver returned.
     *
     * @param array $resolvedData
     * @return string[]
     */
    public function getIdentities(array $resolvedData): array
    {
        // Copied into a local because a class constant cannot be interpolated.
        $tag = self::CACHE_TAG;

        $ids = [];
        foreach ($resolvedData['items'] ?? [] as $item) {
            if (!empty($item['uid'])) {
                $ids[] = $tag . '_' . $item['uid'];
                // Shorthand for the line above, producing the same string:
                // $ids[] = "{$tag}_{$item['uid']}";
            }
        }

        if ($ids !== []) {
            // The bare tag first, so purging the whole FAQ drops every entry.
            array_unshift($ids, $tag);
        }

        return $ids;
    }
}

// Output for ['items' => [['uid' => '7'], ['uid' => '9']]]:
// ['learning_faq', 'learning_faq_7', 'learning_faq_9']

When to use it

  • Make a custom storefront query cacheable and invalidate it when its records change.
  • Work out why a products query returns a MISS on every request.
  • Keep a logged-in customer's data out of a shared Varnish entry.
  • Vary cached GraphQL responses on a dimension Magento does not hash by default.

More examples

Opting a query in, and out

These three forms cover every case you will write. Leaving @cache off is not the same as cacheable: false: the first is simply never cached, while the second actively marks the whole response uncacheable, including the cacheable queries sitting beside it in the same document. Run bin/magento cache:clean config after editing, then check the response headers for X-Magento-Tags to confirm the identity class ran.

Example · graphql
# app/code/Learning/Faq/etc/schema.graphqls

type Query {
    # Public data. Cacheable, with an identity class that produces the tags.
    faqEntries(pageSize: Int = 20): FaqEntries
    @resolver(class: "Learning\\Faq\\Model\\Resolver\\FaqEntries")
    @cache(cacheIdentity: "Learning\\Faq\\Model\\Resolver\\FaqEntries\\Identity")

    # Per-customer data. Never shared.
    myFaqVotes: [FaqVote]
    @resolver(class: "Learning\\Faq\\Model\\Resolver\\MyFaqVotes")
    @cache(cacheable: false)

    # No @cache at all: produces no tags, so it is simply never cached.
    faqSearchSuggestions(term: String!): [String]
    @resolver(class: "Learning\\Faq\\Model\\Resolver\\FaqSuggestions")
}

Adding a factor to the cache id

Reach for this when your responses vary on something Magento does not already hash: a B2B price list, a locale you resolve yourself, a partner id from a header. Core ships three providers of this kind for customer group, tax rate and logged-in status. Keep the value coarse. Every distinct value is a separate cache entry, so a factor with many values trades the cache away for correctness you may already have.

Example · php
<?php
// app/code/Learning/Faq/Model/CacheId/LocaleProvider.php
declare(strict_types=1);

namespace Learning\Faq\Model\CacheId;

use Magento\GraphQl\Model\Query\ContextInterface;
use Magento\GraphQlCache\Model\CacheId\CacheIdFactorProviderInterface;
use Magento\Store\Api\Data\StoreInterface;

class LocaleProvider implements CacheIdFactorProviderInterface
{
    public const NAME = 'FAQ_LOCALE';

    public function getFactorName(): string
    {
        return self::NAME;
    }

    public function getFactorValue(ContextInterface $context): string
    {
        /** @var StoreInterface $store */
        $store = $context->getExtensionAttributes()->getStore();

        return (string) $store->getCode();
    }
}

Registering the factor

The argument name must be idFactorProviders, and the array is merged with the providers core registers rather than replacing them. The file belongs under etc/graphql/ because the calculator only runs in that area. Run bin/magento cache:flush, and setup:di:compile in production mode. Then send the same query twice and compare the X-Magento-Cache-Id header across two store views: the hash should differ.

Example · xml
<?xml version="1.0"?>
<!-- app/code/Learning/Faq/etc/graphql/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\GraphQlCache\Model\CacheId\CacheIdCalculator">
        <arguments>
            <argument name="idFactorProviders" xsi:type="array">
                <item name="faqlocale" xsi:type="object">Learning\Faq\Model\CacheId\LocaleProvider</item>
            </argument>
        </arguments>
    </type>
</config>

Discussion

  • Be the first to comment on this lesson.