HTTP, Persisted Queries, Response Cache, Edge Caching
GraphQL schickt Queries als POST-Request an einen einzigen Endpunkt – das macht Standard-HTTP-Caching schwieriger als in REST. Wer GraphQL-Performance ernst nimmt, muss alle Caching-Schichten kennen: Persisted Queries für HTTP-Caching, Resolver-Level Response Cache und CDN-Edge-Caching für nicht-personalisierte Inhalte.
Inhaltsverzeichnis
- 1. Warum Caching in GraphQL komplexer ist als in REST
- 2. HTTP-Caching und die POST-Problem
- 3. Persisted Queries: HTTP-Caching für GraphQL aktivieren
- 4. Response Cache auf Resolver-Ebene
- 5. Redis als Caching-Backend: Patterns und Konfiguration
- 6. Edge Caching mit CDN und Varnish
- 7. Cache-Invalidierung: Tag-basiert und präzise
- 8. Caching in Magento GraphQL: Praxis und Fallstricke
- 9. Caching-Schichten im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Caching in GraphQL komplexer ist als in REST
In REST-APIs ist HTTP-Caching straightforward: GET-Requests für /products/42 können von CDN, Browser und Proxies gecacht werden. Der URL ist der Cache-Key. In GraphQL hingegen sind alle Anfragen POST-Requests an denselben Endpunkt (/graphql) – POST-Requests werden per Definition nicht gecacht. Das bedeutet, dass jede Query, egal ob für Produktlisten, Kategorien oder CMS-Seiten, ungecacht durch alle Caching-Schichten hindurch auf den Applikationsserver trifft. Bei hohem Traffic ist das ein erheblicher Performance-Nachteil.
Hinzu kommt die Granularität: In GraphQL kann eine einzige Query Daten aus verschiedenen Backend-Systemen mit unterschiedlichen Cache-Lebenszeiten zusammenführen. Eine Query, die gleichzeitig statische CMS-Inhalte (Cache-TTL: Stunden) und dynamische Lagerstandsdaten (Cache-TTL: Minuten) abfragt, lässt sich nicht mit einem einzigen HTTP-Cache-Header abbilden. Effektives Caching in GraphQL erfordert deshalb ein mehrschichtiges Konzept, das auf die Charakteristik jedes einzelnen Felds und Resolvers abgestimmt ist.
2. HTTP-Caching und das POST-Problem
Das POST-Problem ist der zentrale Ausgangspunkt für jede GraphQL-Caching-Strategie. Während GET /products?search=bag von CDNs und Browser-Caches problemlos gecacht wird, landet POST /graphql mit dem Query-Body immer beim Applikationsserver. Es gibt zwei Wege, dieses Problem zu umgehen. Der erste Weg: Queries können auch als GET-Request mit dem Query-String URL-kodiert gesendet werden (GET /graphql?query={products{items{sku}}}). Das funktioniert für kurze Queries, stößt aber bei komplexen Queries an URL-Längenbeschränkungen.
Der zweite und robustere Weg sind Persisted Queries: die Query wird vorab serverseitig unter einem Hash-ID registriert. Der Client sendet dann nur GET /graphql?hash=abc123 – ein kurzer, cachebarer GET-Request. Beide Methoden ermöglichen prinzipiell HTTP-Caching, aber nur Persisted Queries skalieren zuverlässig für produktive GraphQL-APIs. Der Schlüssel liegt darin, dass der Cache-Key stabil und vorhersagbar ist, was bei dynamisch generierten Query-Strings schwieriger zu garantieren ist.
3. Persisted Queries: HTTP-Caching für GraphQL aktivieren
Persisted Queries sind ein zweistufiger Mechanismus. In der ersten Phase registriert das Frontend-Build-System oder ein Bootstrap-Script alle bekannten Queries beim Server. Jede Query erhält einen deterministischen Hash (meist SHA256 des Query-Strings). In der zweiten Phase sendet der Client nur noch den Hash via GET-Request – der Server sucht die Query anhand des Hashes in seiner Registry und führt sie aus. Da der Request jetzt ein GET mit einem stabilen Hash als Parameter ist, kann CDN, Varnish oder Nginx ihn normal cachen.
Der praktische Vorteil geht über Caching hinaus: da der Server nur noch registrierte Queries akzeptiert, können beliebige oder manipulierte Query-Bodies vom Client in der Produktion abgelehnt werden. Das ist ein erheblicher Sicherheitsgewinn. Apollo Client, Relay und andere GraphQL-Clients unterstützen Persisted Queries nativ. Für Magento-Headless-Frontends mit Next.js oder Nuxt kann die Query-Registrierung als Build-Step in die CI-Pipeline integriert werden, sodass neue Queries automatisch beim Server registriert werden, bevor das Frontend deployed wird.
# Persisted Query workflow
# Step 1 (build time): Register query with hash
# POST /graphql
# { "query": "query ProductList { products(search: \"bag\") { items { sku name } } }",
# "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "abc123..." } } }
# Step 2 (runtime): Client sends only the hash via GET
# GET /graphql?extensions={"persistedQuery":{"version":1,"sha256Hash":"abc123..."}}
# → Server looks up query by hash, executes, returns result
# → CDN/Varnish can cache GET responses by hash + query variables
# Step 3 (CDN caching): Only for public, non-personalized data
# Cache-Control: public, max-age=300 (5 minutes for product data)
# Vary: Accept-Language, Store-Code (vary by store view)
# For authenticated/personalized queries: use response cache at resolver level
# Never edge-cache responses that contain customer-specific data
4. Response Cache auf Resolver-Ebene
Der Response Cache auf Resolver-Ebene ist die zweite Caching-Schicht und funktioniert unabhängig vom HTTP-Protokoll. Er ist besonders nützlich für teure Resolver-Operationen, die denselben Output für gleiche Inputs produzieren – unabhängig davon, ob der Request als GET oder POST kommt. Das Pattern: der Resolver prüft einen In-Memory-Cache (Redis, Memcached) auf einen Cache-Key, der aus den Resolver-Argumenten und optionalen Kontext-Daten berechnet wird. Bei Cache-Hit wird das gespeicherte Ergebnis sofort zurückgegeben. Bei Cache-Miss wird die eigentliche Operation ausgeführt und das Ergebnis mit einem TTL in den Cache geschrieben.
Der Cache-Key-Design ist dabei kritisch. Für öffentliche Daten wie Produktlisten oder Kategorien reicht ein Key aus Query-Argumenten (Suchbegriff, Filter, Sortierung). Für Store-View-abhängige Daten muss der Store-Code in den Key. Für personalisierte Daten sollte kein Response Cache verwendet werden – hier ist der Performance-Gewinn durch Caching selten den Komplexitätsaufwand wert, und Datenschutzprobleme entstehen, wenn personalisierte Daten versehentlich geteilt werden.
5. Redis als Caching-Backend: Patterns und Konfiguration
Redis ist das Standard-Caching-Backend für produktive GraphQL-APIs und Magento. Für GraphQL-Response-Caching eignen sich vor allem String-Keys mit JSON-serialisierten Werten und TTL-Ablauf. Das Pattern: der Cache-Key enthält einen Präfix (z.B. gql:product:), die Hash-ID der Query und einen Hash der Argumente. Das verhindert Key-Kollisionen zwischen verschiedenen Query-Typen und ermöglicht gezielte Invalidierung über Key-Präfixe.
Für Tag-basierte Invalidierung – das wichtigste Pattern für präzisen Cache-Clear – speichert man zusätzlich zu jedem Cache-Eintrag die zugehörigen Cache-Tags in einem Set. Wenn ein Produkt aktualisiert wird, iteriert man über alle Sets, die den Produkt-Tag enthalten, und löscht die zugehörigen Cache-Keys. Dieses Pattern ist in Magento über das Cache-Tag-System bereits für Varnish etabliert und lässt sich analog für Redis-basierte GraphQL-Response-Caches implementieren.
6. Edge Caching mit CDN und Varnish
Edge Caching über CDN oder Varnish ist die leistungsstärkste Caching-Schicht, weil sie Requests abfängt, bevor sie den Applikationsserver überhaupt erreichen. Für GraphQL funktioniert das ausschließlich mit Persisted Queries via GET-Request. Die Konfiguration am CDN ist dann analog zu REST: Cache-Control-Header steuern TTL und Vary-Parameter. Wichtig: ausschließlich nicht-personalisierte, öffentliche Daten gehören in den Edge-Cache. Produktlisten, Kategorieseiten, CMS-Blöcke und statische Konfigurationsdaten sind gute Kandidaten.
Der kritische Fallstrick beim Edge Caching: wenn ein eingeloggter Kunde eine Query sendet, die dieselbe Hash-ID wie eine öffentliche Query hat, aber im Authorization-Header einen Token trägt, darf der Edge-Cache die öffentliche Version nicht zurückgeben. Das Vary: Authorization-Header-Pattern stellt sicher, dass Requests mit und ohne Token als verschiedene Cache-Keys behandelt werden. Varnish-Konfigurationen für Magento nutzen dieses Prinzip bereits für traditionelle HTTP-Requests und müssen für GraphQL-GET-Requests entsprechend erweitert werden.
7. Cache-Invalidierung: Tag-basiert und präzise
Das bekannte Sprichwort gilt auch für GraphQL: Cache-Invalidierung ist eines der schwierigsten Probleme in der Informatik. Zu aggressive Invalidierung (bei jeder Änderung alles leeren) eliminiert den Caching-Vorteil. Zu konservative Invalidierung (Inhalte zu lange cachen) führt zu veralteten Daten im Frontend. Das Tag-basierte Invalidierungsmuster ist der beste Kompromiss: jeder Cache-Eintrag wird mit Tags versehen, die beschreiben, welche Daten er enthält.
In Magento sind Cache-Tags bereits ein etabliertes Konzept: Produkte tragen Tags wie cat_p_42, Kategorien cat_c_7. Diese Tags werden bei Aktualisierungen über Observer-Events gesammelt und gezielt invalidiert. Für GraphQL-Response-Cache-Einträge sollte dasselbe Tag-System genutzt werden: ein Resolver, der Produktdaten zurückgibt, annotiert den Cache-Eintrag mit den Produkt-IDs aller zurückgegebenen Produkte. Wenn Produkt 42 aktualisiert wird, werden nur Cache-Einträge mit dem Tag cat_p_42 invalidiert – nicht der gesamte Cache.
# Cache-tag-based invalidation concept for GraphQL resolvers
# Query that fetches products — resolver annotates cache entry with product IDs
query CategoryProducts {
categoryList(filters: { ids: { in: ["3"] } }) {
name
products(pageSize: 24, currentPage: 1) {
items {
id
sku
name
price_range {
minimum_price {
final_price { value currency }
}
}
}
}
}
}
# Resolver pseudo-code for cache with tags:
# cacheKey = "gql:cat:3:page:1:sort:default"
# cacheTags = ["cat_c_3", "cat_p_101", "cat_p_102", "cat_p_103", ...]
# store in Redis: SET gqlcache:{cacheKey} {json} EX 300
# store tag index: SADD gqltag:cat_p_101 gqlcache:{cacheKey}
#
# On product 101 update event:
# SMEMBERS gqltag:cat_p_101 → [gqlcache:{cacheKey}, ...]
# DEL gqlcache:{cacheKey} (targeted invalidation, not full flush)
8. Caching in Magento GraphQL: Praxis und Fallstricke
Magento hat ein eigenes Response-Caching-System für GraphQL, das über den X-Magento-Cache-Id-Header gesteuert wird. Nicht-personalisierte Anfragen erhalten einen deterministischen Cache-Key aus Store-View, Währung und Kundengruppe (Gast). Personalisierte Anfragen – mit Authorization-Token – werden nicht gecacht. Dieses System funktioniert out-of-the-box für Standard-Queries, kann aber bei Custom-Resolvern, die externe Daten einbinden, zu veralteten Ergebnissen führen.
Der häufigste Fallstrick in Magento: ein Custom-Resolver lädt Daten aus einem externen CRM oder ERP. Der Response Cache von Magento weiß nichts von diesen externen Daten und kann sie nicht gezielt invalidieren. Die Lösung: für externe Daten entweder den Cache-Key erweitern (TTL-basiert auf die Aktualisierungsfrequenz der externen Quelle abstimmen) oder den Response Cache für diese spezifischen Queries deaktivieren. Es ist wichtig, diese Entscheidungen explizit zu dokumentieren – sonst entstehen schwer reproduzierbare Cache-Bugs, bei denen Daten im Frontend korrekt erscheinen, aber Stunden veraltet sind.
9. Caching-Schichten im Vergleich
Jede Caching-Schicht hat ihre eigene Charakteristik, Einsatzbereich und Einschränkungen. Eine durchdachte GraphQL-Caching-Strategie kombiniert typischerweise mehrere Schichten – nicht alle auf einmal, sondern gezielt je nach Anforderung der jeweiligen Query.
| Caching-Schicht | Mechanismus | Geeignet für | Einschränkung |
|---|---|---|---|
| HTTP / Browser Cache | GET + Cache-Control Headers | Statische, öffentliche Inhalte | Nur mit Persisted Queries via GET |
| CDN / Edge Cache | Varnish, Cloudflare, Fastly | Produktlisten, Kategorien, CMS | Nie für personalisierte Daten |
| Magento GraphQL Cache | X-Magento-Cache-Id Header | Standard-Queries ohne Custom-Resolver | Keine Kontrolle über externe Daten |
| Redis Response Cache | Resolver-Level, Tag-basiert | Teure Resolver, Custom-Daten | Implementierungsaufwand |
| In-Memory (PHP Request) | Array-Cache innerhalb eines Requests | N+1-Vermeidung im selben Request | Nur für die Dauer eines Requests |
Die optimale Strategie für eine Magento-Headless-Applikation: Persisted Queries für alle öffentlichen, nicht-personalisierten Queries – diese können dann über CDN gecacht werden. Magento's eingebauter GraphQL Response Cache für Standard-Queries. Redis-basierter Custom-Cache für eigene Resolver mit externen Datenquellen. In-Memory-Collector für N+1-Batching innerhalb eines einzelnen Requests. Personalisierte Queries werden nicht gecacht – hier zählen Resolver-Optimierung und N+1-Vermeidung.
Mironsoft
GraphQL-Performance, Caching-Architektur und Magento-Optimierung
GraphQL-APIs, die auch unter Peak-Traffic performen?
Wir analysieren bestehende GraphQL-Setups auf Caching-Lücken, implementieren Persisted Queries, konfigurieren Redis-Response-Caches und richten Tag-basierte Invalidierung für Magento ein.
Persisted Queries
Implementierung und Build-Step-Integration für HTTP-Caching via CDN
Redis Response Cache
Tag-basierter Cache für Custom-Resolver mit präziser Invalidierung
CDN-Konfiguration
Varnish und CDN für GraphQL-GET-Requests konfigurieren und testen
10. Zusammenfassung
Caching in GraphQL erfordert ein mehrschichtiges Konzept, weil POST-Requests standardmäßig nicht gecacht werden und verschiedene Felder unterschiedliche Cache-Charakteristiken haben. Persisted Queries lösen das POST-Problem, indem sie HTTP-Caching via GET-Request ermöglichen. Response Cache auf Resolver-Ebene mit Redis beschleunigt teure Operationen unabhängig vom Protokoll. Edge Caching mit CDN oder Varnish eliminiert Backend-Requests für öffentliche, nicht-personalisierte Inhalte vollständig. Tag-basierte Invalidierung stellt sicher, dass gecachte Daten präzise und ohne globalen Cache-Clear aktualisiert werden können.
In Magento-Projekten bedeutet das konkret: Persisted Queries im Frontend-Build-Step integrieren, Magento's eingebauten GraphQL-Cache für Standard-Queries nutzen und Custom-Resolver mit Redis-Cache und Tag-basierter Invalidierung ausstatten. Der wichtigste Grundsatz: personalisierte Daten nie in den Edge-Cache. Bei allen anderen Daten gilt: je weiter vorn im Stack gecacht wird, desto besser die Performance für alle Nutzer.
Caching in GraphQL — Das Wichtigste auf einen Blick
Persisted Queries
Hash-registrierte Queries ermöglichen GET-Requests. CDN und Browser können GraphQL-Responses für öffentliche Inhalte cachen. Sicherheitsgewinn als Nebeneffekt.
Response Cache
Resolver-Level-Cache mit Redis. Cache-Key aus Query-Argumenten + Store-Code. TTL je nach Aktualisierungsfrequenz der Daten kalibrieren.
Tag-basierte Invalidierung
Cache-Einträge mit Produkt-/Kategorie-Tags versehen. Bei Aktualisierung gezielt nur betroffene Einträge löschen. Kein globaler Cache-Clear.
Personalisierung
Personalisierte Queries nie in Edge-Cache. Hier zählen N+1-Vermeidung und Resolver-Optimierung. Vary: Authorization schützt Edge-Cache vor Datenlecks.
11. FAQ: Caching in GraphQL
1Warum ist Caching in GraphQL schwieriger?
2Was sind Persisted Queries?
3Response Cache auf Resolver-Ebene?
4Personalisierte Daten im Edge-Cache?
5Was ist Tag-basierte Invalidierung?
6Magento's eingebautes GraphQL-Caching?
7Wann keinen Response Cache verwenden?
8Gute Kandidaten für Edge-Caching?
9Persisted Queries in Next.js + Magento?
10Wichtigste Caching-Praxisregel?
Effektives GraphQL-Caching erfordert das Zusammenspiel mehrerer Schichten — von HTTP über Redis bis zum CDN-Edge — und muss stets zusammen mit der Invalidierungsstrategie geplant werden, um Stale-Data-Probleme und unerklärliche Cache-Misses zu vermeiden.
Persisted Queries sind dabei der entscheidende Enabler: Sie verwandeln POST-Only-Endpoints in cacheable GET-Requests und geben Varnish, Fastly oder Cloudflare die Möglichkeit, GraphQL-Responses auf Edge-Ebene zu beschleunigen.