Header, Tags und Invalidierung für performante Storefronts
Ein GraphQL-Cache in Varnish ist kein Automatismus, sondern eine bewusste Architekturentscheidung: Persisted Queries machen aus POST-Anfragen cachebare GET-Requests, X-Magento-Tags ermöglichen präzise Invalidierung, und IdentityInterface entscheidet, welche Resolver überhaupt zum Cache beitragen dürfen. Dieser Artikel zeigt den vollständigen Weg von der Anfrage bis zur Invalidierung.
Inhaltsverzeichnis
- 1. Warum GraphQL-POST-Requests standardmäßig nicht cachebar sind
- 2. Persisted Queries und GET-Requests als Voraussetzung für Full-Page-Caching
- 3. Cache-Header verstehen: Cache-Control, X-Magento-Cache-Id, X-Magento-Tags
- 4. Eigene Resolver cache-fähig machen: IdentityInterface
- 5. Varnish VCL für /graphql anpassen
- 6. Cache-Invalidierung bei Datenänderungen
- 7. Query-Komplexität und Rate Limiting als Vorstufe zum Caching
- 8. Fallstricke bei personalisierten Feldern
- 9. Monitoring der Cache-Effektivität
- 10. Zusammenfassung
- 11. FAQ
1. Warum GraphQL-POST-Requests standardmäßig nicht cachebar sind
HTTP-Caching in Varnish basiert im Kern auf einem einfachen Modell: die Kombination aus HTTP-Methode und URL (ergänzt um ausgewählte Header via Vary) bildet den Cache-Key. Ein GraphQL-Endpoint widerspricht diesem Modell fundamental, weil er als einzige URL /graphql mit der HTTP-Methode POST bedient wird, egal ob eine Produktdetailseite, eine Kategorie mit Facetten oder eine CMS-Landingpage angefragt wird. Die eigentliche Anfrage, also welche Felder, welche Argumente und welche verschachtelten Objekte gewünscht sind, steckt ausschließlich im Request-Body. Für Varnish sehen zwei völlig unterschiedliche GraphQL-Cache-Anfragen identisch aus: gleiche Methode, gleiche URL, gleicher Content-Type.
Varnish liest den Body von POST-Requests standardmäßig nicht in den Hash-Key ein, und das aus gutem Grund: GraphQL-Query-Bodies können beliebig groß, beliebig tief verschachtelt und mit beliebigen Variablen kombiniert werden. Würde man den kompletten Body hashen, entstünde ein Cache-Key pro exakter Zeichenkette, was bei minimal unterschiedlicher Formatierung (Leerzeichen, Feldreihenfolge, Variablennamen) zu einer Explosion an Cache-Einträgen mit fast keiner Trefferquote führen würde. Die Konsequenz für Storefronts mit hoher Last: ausgerechnet die Anfragen, die am häufigsten wiederholt werden (Kategorieseiten, Produktsuche, Header- und Footer-Daten), passieren Varnish ungebremst und landen direkt bei PHP-FPM und der Datenbank.
Ein funktionierender GraphQL-Cache muss deshalb an der Transport-Ebene ansetzen, nicht am Datenmodell. Die Lösung besteht nicht darin, Varnish beizubringen, POST-Bodies zu hashen, sondern darin, GraphQL-Anfragen so umzuformen, dass sie wieder dem GET-plus-URL-Modell entsprechen, für das HTTP-Caches gebaut wurden. Genau das leisten Persisted Queries, kombiniert mit den cache-relevanten Headern, die Magentos GraphQL-Stack pro Response ausliefert.
2. Persisted Queries und GET-Requests als Voraussetzung für Full-Page-Caching
Eine Persisted Query kehrt das Verhältnis zwischen Client und Server um: statt bei jedem Request die komplette Query-Struktur im Body mitzuschicken, registriert der Client die Query einmalig gegen einen stabilen Hash, meist einen SHA-256-Hash über den Query-Text. Anschließend genügt es, diesen Hash als GET-Parameter zu übertragen. Das Automatic-Persisted-Queries-Protokoll (APQ), wie es Apollo Client implementiert, funktioniert dabei zweistufig: der Client sendet zunächst nur den Hash per GET, der Server prüft, ob er die Query bereits kennt, und antwortet im Fehlerfall mit PersistedQueryNotFound. Erst dann schickt der Client die volle Query einmalig per POST nach, danach genügt für alle folgenden Aufrufe der Hash. Für Varnish bedeutet das: aus einer beliebig komplexen POST-Anfrage wird eine GET-Anfrage mit einer festen, kurzen Query-String-Struktur, die sich problemlos hashen und als Cache-Key verwenden lässt.
Magentos /graphql-Endpoint unterstützt GET-Requests bereits im Kern und akzeptiert query, variables und operationName als Query-String-Parameter. Das allein löst noch keinen GraphQL-Cache, weil die vollständige Query weiterhin in der URL steht und je nach Client-Formatierung variieren kann, ist aber die technische Voraussetzung, auf der Persisted Queries aufsetzen. In Kombination mit dem extensions.persistedQuery-Feld aus dem APQ-Protokoll wird aus dem Query-String ein stabiler, kurzer Hash statt einer beliebig langen Zeichenkette.
In der Praxis lohnt sich Persisted-Query-Caching vor allem für wiederkehrende, seitenweite Queries: Header-Navigation, Kategoriebäume, CMS-Blöcke und Produktlisten-Queries mit stabilen Argumenten. Individuelle, ad-hoc zusammengesetzte Queries aus einem GraphQL-Playground oder aus experimentellen Frontend-Features profitieren kaum, weil sich der Hash bei jeder Strukturänderung ändert und der GraphQL-Cache dafür laufend neu aufgebaut werden müsste.
# Automatic Persisted Query (APQ) request as GET - stable, cacheable cache key
# The client only sends the sha256 hash of the previously registered query,
# not the query text itself. Varnish can hash this URL like any static GET request.
curl -G 'https://mironsoft.de/graphql' \
--data-urlencode 'extensions={"persistedQuery":{"version":1,"sha256Hash":"c00590fae0a1a4396a5f5f4cf35a68b8"}}' \
--data-urlencode 'variables={"sku":"MS-2026"}' \
-H 'Store: default' \
-H 'Content-Currency: EUR'
# First-time registration flow (server does not know the hash yet):
# 1. GET with hash only -> server responds: {"errors":[{"message":"PersistedQueryNotFound", ...}]}
# 2. Client resends once via POST with both query text and matching sha256Hash
# 3. All subsequent requests use step 1 (GET + hash) and are fully cacheable
3. Cache-Header verstehen: Cache-Control, X-Magento-Cache-Id, X-Magento-Tags
Sobald eine GraphQL-Anfrage als GET bei Magento ankommt, entscheidet das Modul Magento_GraphQlCache pro Response, ob und wie sie gecacht werden darf. Zentral ist dabei die Klasse Magento\GraphQlCache\Model\CacheableQuery, die während der Resolver-Ausführung als Extension Attribute im Context verfügbar ist und alle Cache-Tags sammelt, die von den beteiligten Resolvern beigetragen werden. Ist die Query am Ende als cachebar markiert, liefert Magento einen Cache-Control-Header mit max-age und public aus, ergänzt um X-Magento-Cache-Id als eindeutigen Identifikator der konkreten Antwort und X-Magento-Tags mit der kommagetrennten Liste aller Cache-Tags, die diese Antwort betreffen.
X-Magento-Cache-Id ist der eigentliche Cache-Key-Bestandteil: er wird aus Store-ID, Query-Signatur und relevanten Kontextwerten wie Kundengruppe oder Währung gebildet, damit zwei inhaltlich unterschiedliche Antworten (z. B. unterschiedliche Preise je Kundengruppe) nie denselben Cache-Eintrag treffen. X-Magento-Tags hingegen hat mit dem eigentlichen Caching nichts zu tun, sondern ist ausschließlich für die spätere Invalidierung relevant: Varnish liest diesen Header im vcl_backend_response aus und speichert die Tags zusammen mit dem Objekt, damit ein späterer PURGE-Request gezielt nur die betroffenen Einträge entfernen kann, statt den kompletten GraphQL-Cache zu leeren.
Fehlt einer der beiden Header oder ist Cache-Control auf no-store gesetzt, cacht Varnish die Antwort grundsätzlich nicht, selbst wenn die VCL-Konfiguration technisch dazu in der Lage wäre. Diese Entscheidung trifft also nicht Varnish, sondern Magentos GraphQL-Cache-Layer auf PHP-Seite, basierend darauf, ob alle beteiligten Resolver als cachebar deklariert wurden.
# Inspect the cache-relevant response headers for a persisted GraphQL query
curl -I -G 'https://mironsoft.de/graphql' \
--data-urlencode 'extensions={"persistedQuery":{"version":1,"sha256Hash":"c00590fae0a1a4396a5f5f4cf35a68b8"}}' \
-H 'Store: default'
# Example response headers on a cacheable GraphQL-Cache hit:
# HTTP/2 200
# content-type: application/json
# cache-control: max-age=86400, public, s-maxage=86400
# x-magento-cache-id: 3f9a2e1c8b6d4a7f0e1c2b3a4d5e6f70
# x-magento-tags: cat_p_1024,cat_c_45,catalog_product,MAGE
# x-magento-cache-debug: HIT
# age: 412
4. Eigene Resolver cache-fähig machen: IdentityInterface
Ein eigener GraphQL-Resolver ist erst dann Teil des GraphQL-Caches, wenn er dem Aggregator mitteilt, welche Cache-Tags seine Daten erzeugen. Dafür implementiert der Resolver zusätzlich zu ResolverInterface das Interface Magento\Framework\GraphQl\Query\Resolver\IdentityInterface mit der Methode getIdentities(array $resolvedData): array. Diese Methode erhält die vom Resolver bereits aufgelösten Daten und gibt ein Array von Tags zurück, üblicherweise nach dem Muster bestehender Magento-Entity-Tags wie cat_p_<id> für Produkte oder eigene, modulspezifische Tags.
Implementiert ein Resolver IdentityInterface nicht, bedeutet das nicht automatisch, dass die gesamte Query uncacheable wird, aber es bedeutet, dass dieser Teil der Antwort keine Tags zur Invalidierung beiträgt. Kritischer ist der umgekehrte Fall: ein Resolver, der personalisierte oder session-abhängige Daten liefert und aktiv CacheableQuery::setCacheValidity(false) aufruft, markiert damit die komplette Response als nicht cachebar, unabhängig davon, wie viele andere Resolver in derselben Query cachebare Daten liefern. Ein einziger falsch konfigurierter Resolver kann so den GraphQL-Cache für eine sonst vollständig cachebare Seiten-Query aushebeln.
Die Cache-Tags, die getIdentities() zurückgibt, folgen bewusst denselben Konventionen wie der klassische Magento-Seiten-Cache. Das hat einen praktischen Vorteil: bestehende Invalidierungs-Logik, etwa Observer, die bei einem Produkt-Save Tags an den Cache-Flush übergeben, muss für den GraphQL-Cache nicht neu geschrieben werden, sie profitiert automatisch von denselben Tags.
declare(strict_types=1);
namespace Mironsoft\CatalogGraphQl\Model\Resolver;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Mironsoft\Badges\Api\BadgeRepositoryInterface;
/**
* Resolver for the "product_badges" GraphQL field, declared cacheable via IdentityInterface.
*/
final class ProductBadges implements ResolverInterface, IdentityInterface
{
/**
* @param BadgeRepositoryInterface $badgeRepository Read-only repository for product badge data.
*/
public function __construct(
private readonly BadgeRepositoryInterface $badgeRepository,
) {
}
/**
* @param Field $field GraphQL field configuration.
* @param ContextInterface $context Resolver execution context.
* @param ResolveInfo $info Resolve tree metadata.
* @param array|null $value Parent resolver value, contains the product SKU.
* @param array|null $args GraphQL field arguments.
* @return array Resolved data made available to getIdentities().
* @throws GraphQlInputException When the parent SKU is missing.
*/
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null): array
{
$sku = $value['sku'] ?? null;
if ($sku === null) {
throw new GraphQlInputException(__('SKU is missing from the parent resolver.'));
}
return [
'sku' => $sku,
'badges' => $this->badgeRepository->getBySku($sku),
];
}
/**
* @param array $resolvedData Data returned from resolve(), used to derive cache tags.
* @return string[] Cache tags that invalidate this field when the underlying product changes.
*/
public function getIdentities(array $resolvedData): array
{
if (!isset($resolvedData['sku'])) {
return [];
}
return ['cat_p_' . $resolvedData['sku']];
}
}
5. Varnish VCL für /graphql anpassen
Die Standard-VCL von Magento (vcl_recv) behandelt /graphql nicht anders als jeden anderen Endpoint, was ohne Anpassung dazu führt, dass unterschiedliche Store-Views, Währungen oder Kundengruppen denselben Cache-Eintrag treffen könnten, selbst wenn die Antworten unterschiedliche Preise enthalten. Für einen korrekten GraphQL-Cache muss vcl_recv deshalb explizit sicherstellen, dass GET-Requests an /graphql anhand von Query-String (der den Persisted-Query-Hash enthält) sowie relevanten Headern wie Store, Content-Currency und der Kundengruppe gehasht werden. Ohne diese Erweiterung würde Varnish beispielsweise B2B-Sonderpreise einer Kundengruppe an anonyme Besucher ausliefern.
In vcl_backend_response müssen zusätzlich die von Magento gesetzten X-Magento-Tags gesichert werden, meist über ein Modul wie vmod_header oder direkt über beresp.http.X-Magento-Tags, damit sie für die spätere PURGE-basierte Invalidierung zur Verfügung stehen. Der ohnehin vorhandene Magento-Standard-VCL-Ban-Mechanismus (ban auf Basis von obj.http.X-Magento-Tags) funktioniert für GraphQL-Antworten identisch wie für klassische HTML-Seiten, solange die Tags korrekt durchgereicht werden.
sub vcl_recv {
# Only apply GraphQL-specific caching logic to GET requests on /graphql
if (req.url ~ "^/graphql" && req.method == "GET") {
# Persisted query hash and variables live in the query string -
# this becomes part of Varnish's default hash key automatically.
# Explicitly fold relevant context headers into the request
# so price/currency/customer-group variants never collide.
set req.http.X-Gql-Store = req.http.Store;
set req.http.X-Gql-Currency = req.http.Content-Currency;
set req.http.X-Gql-Customer-Group = req.http.X-Magento-Customer-Group;
} else if (req.url ~ "^/graphql" && req.method == "POST") {
# Non-persisted POST queries bypass the cache entirely - pass through.
return (pass);
}
}
sub vcl_hash {
if (req.url ~ "^/graphql") {
hash_data(req.http.X-Gql-Store);
hash_data(req.http.X-Gql-Currency);
hash_data(req.http.X-Gql-Customer-Group);
}
}
sub vcl_backend_response {
if (bereq.url ~ "^/graphql" && beresp.http.X-Magento-Tags) {
# Preserve tags on the cached object for later ban-based invalidation
set beresp.http.X-Magento-Tags = beresp.http.X-Magento-Tags;
set beresp.ttl = 24h;
set beresp.grace = 6h;
}
}
sub vcl_deliver {
if (req.url ~ "^/graphql") {
set resp.http.X-Magento-Cache-Debug = obj.hits > 0 ? "HIT" : "MISS";
}
}
6. Cache-Invalidierung bei Datenänderungen
Der Invalidierungs-Mechanismus für den GraphQL-Cache unterscheidet sich nicht grundsätzlich vom klassischen Magento-Seiten-Cache: er basiert auf Tags, nicht auf Zeit. Speichert ein Redakteur ein Produkt, sammelt Magento während des Save-Vorgangs alle betroffenen Cache-Tags, üblicherweise cat_p_<id> für das Produkt selbst sowie die Tags aller Kategorien, in denen es verortet ist. Diese Tag-Liste wird an den konfigurierten Cache-Flush-Mechanismus übergeben, der bei einem aktivierten Varnish-Backend einen PURGE- beziehungsweise BAN-Request an alle konfigurierten Varnish-Instanzen sendet.
Weil GraphQL-Resolver über IdentityInterface dieselben Tag-Konventionen verwenden wie der HTML-Seiten-Cache, greift genau derselbe PURGE-Flow: Varnish erhält einen Ban-Request mit einem Ausdruck wie obj.http.X-Magento-Tags ~ cat_p_1024 und entfernt daraufhin alle zwischengespeicherten Objekte, HTML-Seiten wie GraphQL-Responses, die dieses Tag tragen. Ein Produkt-Save invalidiert damit in einem einzigen Schritt sowohl die klassische Produktdetailseite als auch alle GraphQL-Cache-Einträge, die Daten dieses Produkts enthalten haben.
Der praktische Vorteil dieses Modells: es ist kein separates Invalidierungs-System für GraphQL-Cache nötig, das parallel zur bestehenden Cache-Flush-Logik gepflegt werden müsste. Der Nachteil zeigt sich bei zu grob gewählten Tags: verwendet ein Resolver ausschließlich das generische Tag catalog_product statt spezifischer Produkt-IDs, invalidiert bereits die kleinste Preisänderung an einem einzelnen Produkt den kompletten Produkt-Cache, was die Trefferquote des GraphQL-Caches drastisch senkt.
7. Query-Komplexität und Rate Limiting als Vorstufe zum Caching
Ein funktionierender GraphQL-Cache löst nicht automatisch das Problem unbegrenzt verschachtelter Queries. Selbst wenn ein Client denselben Persisted-Query-Hash verwendet und damit theoretisch cachefähig anfragt, bleibt eine tief verschachtelte Query mit vielen verknüpften Objekten (Kategorie mit Produkten mit Varianten mit Bildern mit verwandten Produkten) beim ersten Cache-Miss teuer in der Auflösung, und ein aggressiv niedriger TTL oder eine hohe Cache-Miss-Rate durch viele Varianten kann diese Kosten regelmäßig neu erzeugen. Query-Komplexität muss deshalb unabhängig vom Caching begrenzt werden, als Schutzmaßnahme davor, dass eine einzelne Anfrage überhaupt in den teuren Auflösungspfad gerät.
Magento konfiguriert diese Grenzen über app/etc/env.php im Schlüssel graphql mit den Werten max_query_complexity und max_query_depth. Ersterer begrenzt die Gesamtzahl der angefragten Felder gewichtet nach ihrer Auflösungskosten, letzterer die maximale Verschachtelungstiefe der Query. Überschreitet eine Anfrage diese Grenzen, weist Magento sie bereits vor der Resolver-Ausführung mit einem GraphQL-Fehler zurück, unabhängig davon, ob sie als Persisted Query oder als klassischer POST-Request eintrifft.
In Kombination mit Persisted Queries ergibt sich ein sauberer zweistufiger Schutz: Rate Limiting und Komplexitätsgrenzen verhindern, dass überhaupt teure oder böswillig konstruierte Queries den Resolver-Layer erreichen, während der GraphQL-Cache in Varnish die verbleibenden, gültigen Anfragen so oft wie möglich beantwortet, ohne PHP-FPM erneut zu belasten. Beide Maßnahmen ergänzen sich, ersetzen sich aber nicht gegenseitig.
8. Fallstricke bei personalisierten Feldern
Der gefährlichste Fehler beim Aufbau eines GraphQL-Caches ist das versehentliche Zwischenspeichern personalisierter Daten in einem geteilten Cache. Felder wie customerCart, customer oder ein eigener Resolver für Treuepunkte, individuelle Rabatte oder zuletzt angesehene Produkte dürfen niemals in einem für alle Besucher geteilten Varnish-Cache landen, selbst wenn dieselbe Query auch cachebare Felder wie Produktdaten enthält. Ein Cache-Treffer, der die Warenkorb- oder Kundendaten einer fremden Session ausliefert, ist keine Performance-Optimierung, sondern ein Datenleck.
Das etablierte Pattern zur Trennung: personalisierte Felder werden clientseitig konsequent in eine separate Query ausgelagert, die niemals über den GET-plus-Persisted-Query-Pfad läuft, sondern immer als POST direkt an PHP-FPM geht und dort mit Cache-Control: no-store beantwortet wird. Die cachebare Query (Produktdaten, Kategoriebaum, CMS-Inhalte) und die private Query (Warenkorb, Kundenkonto) werden vom Frontend als zwei getrennte GraphQL-Requests abgesetzt, oft parallel, aber niemals als eine gemeinsame Query mit gemischtem Cache-Verhalten.
Auf Resolver-Ebene lässt sich dieselbe Trennung erzwingen, indem ein personalisierter Resolver aktiv die gesamte Response als nicht cachebar markiert, sobald er ausgeführt wird. Das verhindert, dass ein Entwickler versehentlich ein personalisiertes Feld in eine ansonsten cachebare Seiten-Query einbaut und dadurch den gesamten GraphQL-Cache für diese Seite unbrauchbar macht, ohne dass dieser Effekt beim Testen mit einem eingeloggten Konto überhaupt auffällt.
declare(strict_types=1);
namespace Mironsoft\CustomerGraphQl\Model\Resolver;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\GraphQlCache\Model\CacheableQuery;
use Mironsoft\Loyalty\Api\LoyaltyRepositoryInterface;
/**
* Resolver for customer-specific loyalty points, explicitly marked non-cacheable.
*/
final class CustomerLoyaltyPoints implements ResolverInterface
{
/**
* @param LoyaltyRepositoryInterface $loyaltyRepository Repository for per-customer loyalty balances.
*/
public function __construct(
private readonly LoyaltyRepositoryInterface $loyaltyRepository,
) {
}
/**
* @param Field $field GraphQL field configuration.
* @param ContextInterface $context Resolver execution context.
* @param ResolveInfo $info Resolve tree metadata.
* @param array|null $value Parent resolver value.
* @param array|null $args GraphQL field arguments.
* @return array Resolved loyalty point balance for the authenticated customer.
* @throws GraphQlAuthorizationException When no customer session is present.
* @throws LocalizedException On repository failure.
*/
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null): array
{
if (!$context->getExtensionAttributes()->getIsCustomer()) {
throw new GraphQlAuthorizationException(__('This field requires a logged-in customer.'));
}
// Explicitly force the whole response to bypass the shared GraphQL cache.
// Without this call, a badly composed query could still let a cacheable
// sibling resolver make the personalized response end up in Varnish.
/** @var CacheableQuery $cacheableQuery */
$cacheableQuery = $context->getExtensionAttributes()->getCacheableQuery();
$cacheableQuery->setCacheValidity(false);
return [
'points' => $this->loyaltyRepository->getPointsForCustomer((int) $context->getUserId()),
];
}
}
9. Monitoring der Cache-Effektivität
Ohne Messung bleibt jede GraphQL-Cache-Konfiguration eine Vermutung. Der erste Indikator ist der Debug-Header X-Magento-Cache-Debug, den Varnish in vcl_deliver setzt und der pro Response HIT oder MISS ausgibt. Für kontinuierliches Monitoring eignet sich dieser Header schlecht, weil er einzelne Requests zeigt, aber keine Aggregation liefert. Aussagekräftiger ist die Auswertung der Varnish-eigenen Statistiken via varnishstat beziehungsweise varnishncsa, gefiltert auf den /graphql-Pfad, um die tatsächliche Hit-Rate für GraphQL getrennt von der Hit-Rate für klassische HTML-Seiten zu betrachten.
In der Praxis unterscheidet sich die erreichbare Hit-Rate stark je nach gewählter Cache-Strategie. Ein klassischer REST-Endpoint mit stabiler URL pro Ressource erreicht traditionell die höchste Trefferquote, weil der Cache-Key ohne zusätzliche Maßnahmen bereits stabil ist. GraphQL ohne Persisted Queries erreicht faktisch keine nennenswerte Hit-Rate, weil jede Query-Formatierung einen neuen Cache-Key erzeugt. Erst GraphQL mit Persisted Queries und korrekt konfiguriertem Varnish erreicht wieder eine mit REST vergleichbare Trefferquote, bei gleichzeitig höherer Flexibilität auf Client-Seite.
| Kriterium | REST-Endpoint-Cache | GraphQL ohne Persisted Queries | GraphQL mit Persisted Queries + Varnish |
|---|---|---|---|
| Cache-Key-Stabilität | Hoch, URL ist der Key | Sehr niedrig, Body variiert | Hoch, Hash ist der Key |
| Hit-Rate in der Praxis | 70-90 % | Nahe 0 % | 60-85 % |
| Implementierungsaufwand | Gering | Keiner (aber wirkungslos) | Mittel bis hoch (VCL, Client, IdentityInterface) |
| Personalisierung | Separater Endpoint nötig | Kein Problem, aber auch kein Cache | Erfordert saubere Query-Trennung |
Die Tabelle macht deutlich, dass der Implementierungsaufwand für GraphQL-Cache mit Persisted Queries real ist, sich aber in einer Hit-Rate niederschlägt, die wieder in der Größenordnung klassischer REST-Caches liegt. Wer diesen Aufwand scheut und GraphQL ohne Persisted Queries produktiv betreibt, verschenkt in der Praxis den gesamten Full-Page-Cache-Vorteil für den GraphQL-Traffic, unabhängig davon, wie gut Varnish sonst konfiguriert ist.
10. Zusammenfassung
Ein belastbarer GraphQL-Cache in Varnish entsteht nicht durch eine einzelne Einstellung, sondern durch das Zusammenspiel mehrerer Ebenen: Persisted Queries verwandeln variable POST-Bodies in stabile, hashbare GET-Requests. Die Cache-Header Cache-Control, X-Magento-Cache-Id und X-Magento-Tags transportieren pro Response, ob und wie lange gecacht werden darf und mit welchen Tags eine spätere Invalidierung erfolgt. Eigene Resolver werden über IdentityInterface aktiv Teil dieses Systems, und die Varnish-VCL muss Store, Währung und Kundengruppe explizit in den Hash-Key aufnehmen, damit GraphQL mit Varnish cachen nicht zu falsch ausgelieferten Preisen führt.
Die Invalidierung folgt demselben tag-basierten PURGE-Mechanismus wie der klassische Magento-Seiten-Cache, was Konsistenz schafft, aber auch bedeutet, dass zu grobe Tags die Trefferquote unnötig senken. Query-Komplexitätsgrenzen und personalisierte Felder mit sauberer Query-Trennung sind keine optionalen Ergänzungen, sondern notwendige Schutzmaßnahmen, ohne die ein GraphQL-Cache entweder teure Anfragen ungebremst durchlässt oder private Daten in einem geteilten Cache landen lässt.
GraphQL-Cache mit Varnish: das Wichtigste auf einen Blick
Persisted Queries
Query-Hash statt Query-Body als GET-Request, Voraussetzung dafür, dass Varnish überhaupt einen stabilen Cache-Key bilden kann.
Cache-Header
Cache-Control, X-Magento-Cache-Id und X-Magento-Tags steuern Cachebarkeit, Key und Invalidierung pro Response.
IdentityInterface
Eigene Resolver müssen Cache-Tags aktiv deklarieren, sonst bleiben sie außerhalb des GraphQL-Cache-Systems.
Personalisierte Felder
Kundendaten und Warenkorb gehören in eine getrennte, niemals gecachte Query, nicht in dieselbe Anfrage wie Katalogdaten.
11. FAQ: GraphQL-Cache mit Varnish
1Warum cacht Varnish GraphQL nicht automatisch?
2Was ist eine Persisted Query?
3Reicht GET statt POST allein?
4Wofür ist X-Magento-Tags gut?
5Was, wenn IdentityInterface fehlt?
6Wie personalisierte Daten aus dem Cache halten?
7Warum Kundengruppe im Hash-Key?
8Löst Caching das Problem verschachtelter Queries?
9Wie invalidiere ich bei Produkt-Updates?
10Wie prüfe ich Cache-Treffer?
Mironsoft
GraphQL-Performance, Varnish und Full-Page-Cache für Magento-Storefronts
GraphQL-Cache, der eure Storefront wirklich entlastet?
Wir analysieren eure GraphQL-Traffic-Muster, richten Persisted Queries ein und konfigurieren Varnish so, dass Cache-Hit-Rate, Invalidierung und Personalisierung zusammenpassen, statt sich gegenseitig auszuhebeln.
Cache-Audit
Analyse von Hit-Rate, Cache-Headern und Tag-Konventionen im bestehenden GraphQL-Cache
Persisted Queries
Einführung von Persisted Queries inklusive VCL-Anpassung für stabile Cache-Keys
Invalidierung
Saubere IdentityInterface-Resolver und Tag-Konventionen für präzises Purging