sinnvoll kombinieren
GraphQL und Caching gelten oft als schwieriges Paar – weil alle Anfragen auf denselben Endpunkt zeigen und POST-Requests von CDNs standardmäßig nicht gecacht werden. Mit der richtigen Kombination aus Resolver-Caching auf Applikationsebene und Edge-Caching über Persisted Queries lässt sich das Problem systematisch lösen.
Inhaltsverzeichnis
- 1. Warum GraphQL und Caching schwierig zusammenpassen
- 2. Die zwei Cache-Ebenen: Resolver und Edge
- 3. Resolver-Caching: Strategie und Cache-Key-Design
- 4. Edge-Caching mit Persisted Queries
- 5. Cache-Invalidierung: das schwierigste Problem
- 6. Caching-Strategien in Magento GraphQL
- 7. Falsch / Richtig bei GraphQL-Caching
- 8. Typische Fehlerbilder beim kombinierten Caching
- 9. Caching-Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum GraphQL und Caching schwierig zusammenpassen
Das grundlegende Problem von GraphQL und HTTP-Caching liegt in der Transportschicht: GraphQL nutzt standardmäßig HTTP POST, und POST-Requests werden von CDNs und Proxies nicht gecacht. Im Gegensatz zu REST, wo jeder Endpunkt eine eigene URL hat und GET-Requests problemlos gecacht werden, landen alle GraphQL-Anfragen auf demselben Endpunkt (/graphql) mit unterschiedlichem Body. Ein CDN sieht nur eine POST-Anfrage an eine URL und übergeht die Cache-Konfiguration vollständig.
Das bedeutet nicht, dass GraphQL nicht cachebar ist – es bedeutet, dass man aktiv eine Caching-Strategie aufbauen muss, statt sich auf das Standardverhalten zu verlassen. Die gute Nachricht: es gibt zwei Ebenen, auf denen man GraphQL effektiv cachen kann, und beide ergänzen sich sinnvoll. Die erste Ebene ist der Resolver-Cache auf Applikationsebene, der teure Datenbankabfragen innerhalb desselben Prozesses vermeidet. Die zweite Ebene ist der Edge-Cache im CDN, der vollständige GraphQL-Antworten über HTTP-Caching zwischenspeichert – ermöglicht durch Persisted Queries als GET-Requests.
2. Die zwei Cache-Ebenen: Resolver und Edge
Das Resolver-Caching arbeitet innerhalb einer einzelnen GraphQL-Anfrage. Wenn mehrere Felder in einer Query denselben Resolver mit denselben Argumenten aufrufen würden – oder wenn verschiedene Queries innerhalb desselben Requests ähnliche Daten anfordern – verhindert ein In-Memory-Cache, dass dieselbe Datenbankabfrage mehrfach ausgeführt wird. Der bekannteste Ansatz ist das DataLoader-Pattern, das Anfragen bündelt und de-dupliziert. In Magento gibt es ähnliche Mechanismen über Cache-Pools und Context-basiertes Caching.
Das Edge-Caching arbeitet auf der Ebene vollständiger HTTP-Responses. Wenn ein CDN oder Proxy eine GraphQL-Antwort zwischenspeichert, bekommt der nächste Client mit der identischen Anfrage die gecachte Antwort direkt vom Edge-Node – ohne dass die Anfrage überhaupt den Applikationsserver erreicht. Das senkt die Latenz dramatisch und reduziert die Last auf dem Server. Die Voraussetzung: die Anfrage muss als GET-Request formuliert sein, was Persisted Queries ermöglichen.
3. Resolver-Caching: Strategie und Cache-Key-Design
Der Cache-Key für einen Resolver muss alle Parameter kodieren, die das Ergebnis beeinflussen: die Resolver-Methode, alle Argumente, die Sprache, den Kundenkontext (anonym vs. eingeloggt) und ggf. den Store-Kontext. Ein Cache-Key, der den Kundenkontext nicht berücksichtigt, ist eine ernstzunehmende Sicherheitslücke: anonym gecachte Daten könnten an eingeloggte Kunden ausgeliefert werden. In Magento ist das besonders kritisch bei Preisen, Beständen und personalisierten Empfehlungen.
Die TTL (Time-to-Live) für Resolver-Caches sollte nach Datentyp differenziert werden. Statische Inhalte wie CMS-Seiten oder Kategorietexte können lange TTLs von Stunden haben. Dynamische Daten wie Preise oder Lagerbestände brauchen kurze TTLs oder müssen event-basiert invalidiert werden. Das DataLoader-Pattern – ursprünglich für Node.js entwickelt, aber konzeptionell auch in PHP umsetzbar – bündelt alle Resolver-Aufrufe für denselben Datentyp innerhalb einer Anfrage in einen einzigen Batch-Datenbankaufruf.
# Query that benefits from both resolver-level and edge-level caching
# Resolver cache: prevents N+1 for category data
# Edge cache: entire response cached at CDN via persisted query GET
query CategoryPageData($categoryId: String!, $pageSize: Int = 20) {
categories(filters: { ids: { in: [$categoryId] } }) {
items {
name
description
products(pageSize: $pageSize) {
total_count
items {
sku
name
price_range {
minimum_price {
final_price { value currency }
}
}
}
}
}
}
}
4. Edge-Caching mit Persisted Queries
Persisted Queries lösen das grundlegende Problem des POST-Cachings: Statt die vollständige Query im Request-Body zu senden, registriert der Client die Query einmalig auf dem Server (oder nutzt vordefinierte Query-Hashes), und sendet danach nur noch den Hash als GET-Parameter. Der Server erkennt den Hash, lädt die zugehörige Query und führt sie aus. Der Client sendet nun GET-Requests mit einem stabilen Hash, und das CDN kann diese Requests wie normale HTTP GET-Anfragen behandeln – mit allen Standard-Caching-Mechanismen.
Die Implementierung von Persisted Queries in Magento erfordert Anpassungen auf Serverseite: ein Endpoint, der hashed Queries entgegennimmt, und eine Storage-Schicht, die die Query-Maps persistent hält. Apollo Client und andere GraphQL-Clients unterstützen Persisted Queries out-of-the-box. Die wichtigste Konfiguration auf CDN-Seite: Cache-Control-Header müssen vom Server für cacheable Queries gesetzt werden, und Vary-Header müssen sicherstellen, dass Store-Context und Sprache als Cache-Dimensionen berücksichtigt werden.
# Persisted Query pattern: client sends hash instead of full query
# GET /graphql?operationName=CategoryPage&extensions={"persistedQuery":{"sha256Hash":"abc123..."}}
# Server looks up query by hash and executes:
query CategoryPage($categoryId: String!) {
categories(filters: { ids: { in: [$categoryId] } }) {
items {
name
products(pageSize: 20) {
items { sku name }
}
}
}
}
# CDN caches the GET response with:
# Cache-Control: public, max-age=300, stale-while-revalidate=60
5. Cache-Invalidierung: das schwierigste Problem
Cache-Invalidierung gilt nicht ohne Grund als eines der schwierigsten Probleme der Softwareentwicklung. Bei GraphQL ist es besonders komplex, weil ein Cache-Eintrag auf Antwortebene viele verschiedene Datenquellen zusammenfasst. Eine Produktpreisänderung kann Dutzende gecachte Antworten auf verschiedenen CDN-Nodes ungültig machen – auf Resolver-Ebene und auf Edge-Ebene gleichzeitig. Wer keine klare Invalidierungsstrategie hat, liefert veraltete Preise oder Bestände aus.
Bewährte Muster für die Invalidierung: Tag-basiertes Caching, bei dem jede gecachte Response mit Tags für die enthaltenen Daten versehen wird (z.B. product_1234, category_56). Bei einer Datenänderung werden alle Responses mit dem entsprechenden Tag ungültig erklärt. Varnish, Fastly und Cloudflare unterstützen Tag-basiertes Purging über Surrogate-Key-Header. In Magento ist dieses Muster für Varnish bereits eingebaut – für GraphQL muss es explizit auf die Cache-Tags der abgefragten Entities ausgedehnt werden.
# Cache tagging concept for GraphQL responses
# Server sets Surrogate-Key or Cache-Tag headers:
# Surrogate-Key: product_1234 product_5678 category_10
# On product price update, purge all tagged responses:
# PURGE /graphql?... with tag: product_1234
query ProductsWithCacheTags($skus: [String!]!) {
products(filter: { sku: { in: $skus } }) {
items {
id
sku
name
price_range {
minimum_price { final_price { value } }
}
}
}
}
6. Caching-Strategien in Magento GraphQL
In Magento 2 gibt es bereits einen eingebauten GraphQL-Cache auf Basis von Varnish und dem Full-Page-Cache. Für anonyme Anfragen wird das Ergebnis gecacht, für authentifizierte Anfragen wird der Cache umgangen. Diese Unterscheidung ist korrekt, aber in vielen Projekten nicht fein genug: Kategorieseiten für eingeloggte Kunden ohne Preis- oder Bestandspersonalisierung könnten teilweise gecacht werden, werden es aber nicht, weil der Auth-Header pauschal den Cache deaktiviert.
Die erweiterte Strategie für Magento: öffentliche Teile von Queries – Kategorietexte, Produktbeschreibungen, Bilder – aus Resolver-Antworten extrahieren und separat cachen, während personalisierte Teile – Preise für eingeloggte Kundengruppen, Bestand nach Lager-Zuordnung – kurze TTLs oder kein Caching erhalten. Das erfordert architektonische Anpassungen: statt einer großen Query liefern zwei kleinere Queries öffentliche und personalisierte Daten getrennt. Die öffentliche Query wird gecacht, die personalisierte nicht.
7. Falsch / Richtig bei GraphQL-Caching
Der häufigste Fehler beim GraphQL-Caching: man deaktiviert den Cache für alle GraphQL-Anfragen, weil eine einzige personalisierte Query den gesamten Endpunkt als nicht-cacheable markiert. Das ist unnötig restriktiv. Mit einer sauberen Query-Trennung zwischen öffentlichen und personalisierten Daten und einer Caching-Strategie, die auf Query-Granularität statt Endpunkt-Granularität operiert, lassen sich auch in Projekten mit eingeloggten Nutzern erhebliche Cache-Hit-Rates erzielen.
| Szenario | Falsch | Richtig | Effekt |
|---|---|---|---|
| Edge-Caching | POST ohne Persisted Queries | GET mit Persisted Query Hash | CDN kann Response cachen |
| Cache-Keys | Ohne Kontext-Parameter | Store + Sprache + Auth-Status | Keine Datenlecks zwischen Kontexten |
| Invalidierung | TTL-only, kein Tag-Purging | Tag-basiertes Purging | Sofortige Konsistenz nach Änderung |
| Personalisierung | Alles ungecacht bei Auth | Öffentliche / private Queries trennen | Hohe Hit-Rate auch für eingeloggte Nutzer |
| N+1 im Resolver | Je Entity eine DB-Query | DataLoader / Batch-Loading | Deutlich weniger DB-Abfragen pro Request |
8. Typische Fehlerbilder beim kombinierten Caching
Das häufigste Fehlerbild beim GraphQL-Caching: veraltete Preise nach Preisänderungen, weil die Invalidierungsstrategie die Preis-Entities nicht als Cache-Tags gesetzt hat. Der Administrator ändert einen Preis im Backend, aber die Storefront zeigt noch eine Stunde den alten Preis – weil der Edge-Cache noch den alten Wert hält und kein Purge-Signal erhalten hat. Dieses Problem ist in REST-Projekten mit klar definierten Ressourcen-URLs leichter zu lösen als in GraphQL, wo eine Antwort viele Entities mischen kann.
Ein zweites Fehlerbild: Cache-Poisoning durch fehlende Vary-Header. Wenn das CDN nicht zwischen verschiedenen Store-Views oder Sprachen unterscheidet und einen deutschen Cache-Eintrag an englischsprachige Besucher ausliefert, entsteht ein subtiler Bug, der in Tests schwer zu reproduzieren ist. Der dritte häufige Fehler: Resolver-Cache ohne Größenbeschränkung, der unbegrenzt wächst und den Speicher des Applikationsservers erschöpft. Jeder In-Memory-Cache braucht eine konfigurierte Maximalgröße und eine Eviction-Policy.
9. Caching-Strategien im Vergleich
Die Wahl der richtigen Caching-Kombination hängt vom Anwendungsfall ab. Ein Headless-Shop mit hauptsächlich anonymen Besuchern profitiert am stärksten von Edge-Caching über Persisted Queries – die meisten Responses können mit langen TTLs gecacht werden. Ein Shop mit vielen eingeloggten Kunden und personalisierten Preisen profitiert mehr von Resolver-Caching mit kurzen TTLs, das die Datenbankabfragen innerhalb eines Requests reduziert, ohne Personalisierungsdaten zu vermischen.
Resolver- und Edge-Caching kombinieren — Das Wichtigste auf einen Blick
Resolver-Cache
DataLoader/Batch-Loading verhindert N+1 innerhalb einer Anfrage. Cache-Key muss Store, Sprache und Auth-Status enthalten.
Edge-Cache
Persisted Queries konvertieren POST zu GET. CDN cached vollständige Responses. Cache-Control + Vary-Header korrekt konfigurieren.
Invalidierung
Tag-basiertes Purging statt TTL-only. Entity-Tags im Response-Header. Purge-Event bei Datenänderungen auslösen.
Personalisierung
Öffentliche und personalisierte Daten in getrennte Queries aufteilen. Öffentliche cachen, personalisierte nicht oder mit kurzer TTL.
10. Zusammenfassung
Resolver-Caching und Edge-Caching kombinieren ist kein einzelnes Feature, sondern eine Architekturentscheidung, die von Anfang an in das Query-Design einfließen muss. Wer Caching als Nachgedanken behandelt, der nach dem Launch hinzugefügt wird, kämpft gegen strukturelle Probleme an: Queries, die öffentliche und private Daten mischen, fehlende Cache-Tags in Resolver-Outputs und fehlende Persisted-Query-Infrastruktur.
Die effektivste Reihenfolge: zuerst Resolver-Caching mit DataLoader-Pattern und klar definiertem Cache-Key-Schema einführen, dann Persisted Queries implementieren und schließlich Edge-Caching mit Tag-basierter Invalidierung aufbauen. Jede Ebene bringt für sich messbare Verbesserungen, und alle drei zusammen machen GraphQL-APIs konkurrenzfähig mit gecachten REST-APIs – ohne die Flexibilität von GraphQL aufzugeben.