Warum ist GraphQL manchmal schnell und manchmal überraschend langsam? Der Grund liegt oft nicht in GraphQL selbst, sondern in der Art, wie Antworten zwischengespeichert werden. Bei REST gibt es häufig „eine URL = eine Ressource“, das passt gut zu klassischen HTTP-Caches. GraphQL arbeitet dagegen meist mit einer einzigen Endpoint-URL, aber sehr vielen möglichen Query-Formen. Genau hier setzt GraphQL Caching an: Es braucht bewusst geplante Ebenen und klare Regeln, damit wiederkehrende Anfragen nicht jedes Mal den vollen Weg bis zur Datenbank gehen.
Warum Caching bei GraphQL anders ist als bei REST
GraphQL-Anfragen werden typischerweise per POST an eine feste URL geschickt, etwa
/graphql
. Ein CDN oder Proxy kann eine POST-Antwort zwar cachen, macht das aber nicht automatisch. Dazu kommt: Zwei Anfragen können dieselbe URL nutzen, aber komplett andere Daten anfordern. Bei REST kann ein Cache häufig nach URL und Headern entscheiden. Bei GraphQL muss klar sein, welche Query (inklusive Variablen) zu welchem Ergebnis führt.
Was genau soll gecacht werden?
In der Praxis gibt es drei sinnvolle Cache-Ziele:
- Response Caching: Die komplette GraphQL-Antwort für eine bestimmte Query wird gespeichert.
- Object Caching: Einzelne Objekte (z. B. User mit ID 42) werden gespeichert, unabhängig von der Query-Form.
- Data-Source-Caching: Ergebnisse aus Datenquellen (Datenbank-Queries, REST-Calls, Microservices) werden zwischengespeichert.
Response Caching wirkt am stärksten, ist aber am schwierigsten korrekt zu invalidieren (zu „entwerten“, wenn Daten sich ändern). Object- oder Data-Source-Caching sind oft robuster, weil sie näher an den tatsächlichen Daten liegen.
Woran scheitert es häufig?
- Unklare Cache-Key-Logik (welche Anfrage ist „gleich“?).
- Vermischung von öffentlichen und nutzerspezifischen Daten (z. B. Preise, Rollen, Permissions).
- Zu aggressive TTL (Ablaufzeit) oder gar keine Invalidation.
- N+1-Probleme, die durch Caching nur „verdeckt“ wirken.
Gerade wenn im Backend ohnehin viele kleine Datenbankabfragen entstehen, hilft zusätzlich ein Blick auf das N+1 Query Problem, weil Caching sonst nur Symptome bekämpft.
Cache-Schichten: Client, CDN, Server – wer macht was?
Stabiles Caching entsteht meist nicht durch eine einzelne Maßnahme, sondern durch eine Kombination aus mehreren Ebenen. Jede Ebene kann andere Dinge gut.
Client-Caching: schnellster Gewinn für wiederkehrende Views
Viele GraphQL-Clients (z. B. Apollo Client, Relay, urql) arbeiten mit einem normalisierten Cache: Daten werden nach Typ und ID gespeichert. Dadurch muss bei Navigation innerhalb der App oft nicht erneut geladen werden. Das passt besonders gut für wiederkehrende UI-Elemente wie Profile, Produktkacheln oder Listen.
Wichtig ist ein konsequentes Schema-Design: Wenn Objekte stabile IDs haben und Typen sauber sind, kann der Client Cache-Treffer zuverlässig erzeugen. Ohne IDs wird aus „Objekt wiederverwenden“ schnell „Antwort jedes Mal neu berechnen“.
CDN- und Edge-Caching: nur für eindeutig öffentliche Antworten
Ein CDN lohnt sich vor allem für Inhalte, die für alle gleich sind (z. B. Content-Seiten, Kategorien, öffentliche Produktinfos). Bei GraphQL ist das schwieriger als bei REST, aber nicht unmöglich: Die Query (und Variablen) müssen im Cache-Key berücksichtigt werden, und personenbezogene Antworten dürfen dort nicht landen.
Praktisch heißt das: öffentliche Queries klar trennen, Auth-Header vermeiden oder streng unterscheiden, und Antworten so ausliefern, dass sie cachebar sind. Wer dafür schon ein gutes Verständnis von HTTP-Caching hat, kann viele typische Fehler vermeiden. Hilfreich ist dazu HTTP Caching mit Cache-Control und ETag, weil die Grundlogik auch bei GraphQL gilt.
Server-Caching: stabil, aber nur mit klarer Invalidation
Auf dem Server sind zwei Varianten besonders gängig:
- Persisted Queries (vorgespeicherte Queries): Der Client sendet nur eine Query-ID statt der gesamten Query.
- Resolver-/Data-Source-Caching: Ergebnisse einzelner Resolver oder Datenquellen werden zwischengespeichert (häufig in Redis oder im Prozessspeicher).
Persisted Queries helfen nicht direkt beim Caching der Daten, aber sie machen CDN/Proxy-Caching einfacher, reduzieren Payload und begrenzen Query-Vielfalt. Resolver-Caching hilft oft sofort, weil die Datenbank entlastet wird.
Cache Keys: Wie Anfragen eindeutig werden
Ein Cache Key ist der „Name“ unter dem eine Antwort gespeichert wird. Bei GraphQL sollte der Key typischerweise aus diesen Teilen bestehen:
- Operation (Query/Mutation) und ggf. Operation-Name
- Query-Text oder Persisted-Query-ID
- Variablen (sortiert/normalisiert)
- Kontext, der das Ergebnis beeinflusst (z. B. Sprache, Währung)
- Auth-Kontext nur, wenn die Antwort nutzerspezifisch ist
Öffentlich vs. personalisiert sauber trennen
Eine der wichtigsten Entscheidungen: Darf diese Antwort geteilt gecacht werden (öffentlich) oder ist sie user-spezifisch (privat)? Sobald Berechtigungen, Rabatte, „gespeicherte Favoriten“ oder individuelle Sichtbarkeit im Spiel sind, muss der Cache entweder pro Nutzer getrennt werden oder die Anfrage darf nicht an einem gemeinsamen Ort (CDN) landen.
Als Faustregel: Wenn ein Auth-Token das Ergebnis verändern kann, ist CDN-Caching nur dann sinnvoll, wenn der Key den Nutzer eindeutig mit abbildet oder die Query explizit ohne Auth funktioniert.
Tabelle: Welche Cache-Form passt zu welchem Datentyp?
| Datentyp | Empfohlene Cache-Ebene | Hinweis |
|---|---|---|
| Öffentlicher Content (z. B. Blog-Teaser) | CDN/Edge + Server | Klare Trennung von personalisierten Feldern |
| Produktdaten ohne Preisregeln | CDN/Edge + Client | Variablen (z. B. locale) im Key berücksichtigen |
| Profil/Account-Daten | Client + Server (privat) | Niemals shared cachen, Invalidation wichtig |
| Listen/Filter (z. B. Suche) | Server (kurze TTL) + Client | Cache-Key muss Filter/Sortierung enthalten |
| Aggregationen/Statistiken | Server (berechnet) + optional CDN | Stale-While-Revalidate kann sinnvoll sein |
Invalidation: Wann ein Cache bewusst „vergessen“ muss
Caching bringt nur dann verlässlich Nutzen, wenn klar ist, wann Einträge veralten. Zwei Wege sind üblich: Ablaufzeit (TTL) oder gezielte Invalidation (z. B. nach einer Mutation).
TTL (Ablaufzeit): einfach, aber nicht immer präzise
TTL heißt: Ein Eintrag gilt z. B. für 60 Sekunden, danach wird neu geholt. Das ist leicht umzusetzen und in vielen Fällen ausreichend, etwa bei Listen, Countern oder Content, der nicht sekündlich korrekt sein muss. Nachteil: Änderungen sind bis zum Ablauf nicht sichtbar.
Invalidation nach Mutations: korrekt, aber nur mit Regeln
Mutations ändern Daten. Nach einer Mutation sollten betroffene Cache-Einträge entwertet werden. Bei normalisierten Client-Caches passiert das oft über Updates: ein Objekt mit ID 42 wurde geändert, also wird der Eintrag aktualisiert. Serverseitig braucht es meist eine eigene Logik (z. B. Tags/Keys pro Entität).
Wichtig ist: Invalidation muss zum Datenmodell passen. Wenn eine Mutation „Produktpreis geändert“ auslöst, sind nicht nur Produkt-Details betroffen, sondern ggf. auch Listen, Empfehlungen oder Warenkorb-Zusammenfassungen.
Praktische Schrittfolge für den Einstieg (ohne Overengineering)
- Öffentliche und personalisierte Queries trennen: Welche Antworten dürfen geteilt gecacht werden, welche nicht?
- Im Client auf normalisiertes Caching setzen: stabile IDs und Typen sicherstellen.
- Im Server zuerst Data-Source/Resolver-Caching einführen: damit Datenbank und externe Services entlastet werden.
- Cache Keys definieren: Query-ID/Query-Text + normalisierte Variablen + relevante Kontext-Parameter.
- TTL für „unkritische“ Daten festlegen, Invalidation für „kritische“ Mutations planen.
- Fehlersuche vorbereiten: Cache-Hit/Miss im Logging sichtbar machen (ohne sensitive Daten zu loggen).
Typische Fragen aus der Praxis: Sicherheit, Debugging, Betrieb
Kann Caching zu Datenleaks führen?
Ja, wenn personalisierte Antworten shared gecacht werden. Das passiert oft unabsichtlich, wenn Auth-Header im CDN ignoriert werden oder der Cache-Key keinen Nutzerbezug enthält. Deshalb: Bei Unsicherheit lieber privat cachen (Client/Server pro Nutzer) und öffentliche Antworten explizit definieren.
Wie bleibt Debugging überschaubar?
Hilfreich sind klare Metriken und Logs: Wurde ein Resolver aus dem Cache bedient? Welche Ebene war es (Client/Server)? Welche TTL galt? Dabei sollten keine Tokens oder personenbezogene Daten in Logs landen. Wer Logging ohnehin strukturieren möchte, kann sich an Structured Logging orientieren, um Cache-Ereignisse sauber zu filtern.
Welche Rolle spielt Query-Komplexität?
Wenn Clients sehr große, tiefe Queries senden, steigen Ausführungszeit und Last. Persisted Queries und Limits (z. B. Depth/Complexity) helfen, die API stabil zu halten. Caching ersetzt diese Limits nicht, es ergänzt sie.
Vergleich: Response-, Object- und Data-Source-Caching
- Response-Caching
- Vorteile: maximale Wirkung bei identischen Requests
- Nachteile: Invalidation schwierig, personalisierte Antworten kritisch
- Object-Caching
- Vorteile: passt gut zu IDs und normalisierten Daten, gute Wiederverwendung
- Nachteile: nicht jede Query lässt sich komplett daraus zusammensetzen
- Data-Source-Caching
- Vorteile: entlastet Datenbank/externe Services, oft schnell umgesetzt
- Nachteile: bringt weniger, wenn Resolver viele Varianten bauen
Wer diese Ebenen kombiniert und bewusst entscheidet, welche Daten wo landen, bekommt eine GraphQL-API, die auch unter Last stabil bleibt. Oft reicht schon eine saubere Trennung von öffentlichen und privaten Antworten plus ein solides Resolver-Caching, um spürbar weniger Datenbankdruck zu haben.

