mit Varnish und CDN Edge Caching
POST-Requests mit dynamischem Body lassen sich von klassischen HTTP-Caches nicht ohne Weiteres cachen, genau das ist aber die Standard-Transportform von GraphQL. GraphQL Response Caching mit Varnish und CDN Edge Caching überwindet diese Hürde durch GET-Queries, Persisted Queries und Surrogate Keys, sodass Antworten am Edge landen, bevor sie den Origin-Server je erreichen.
Inhaltsverzeichnis
- 1. Warum GraphQL-Caching schwerer ist als REST-Caching
- 2. GET statt POST: Voraussetzung für HTTP-Caching
- 3. Varnish-VCL für GraphQL-Queries konfigurieren
- 4. Persisted Queries als Cache-Key-Strategie
- 5. Surrogate Keys für granulare Invalidierung
- 6. CDN Edge Caching: Fastly, Cloudflare, CloudFront
- 7. Cache-Invalidierung bei Mutations: Purge-Strategien
- 8. Cache-Hit-Rate messen und Header richtig setzen
- 9. Edge Caching im Vergleich zu Application-Level-Caching
- 10. Zusammenfassung
- 11. FAQ
1. Warum GraphQL-Caching schwerer ist als REST-Caching
REST-APIs cachen sich fast von selbst: Jede Ressource hat eine eindeutige URL, ein GET-Request auf /products/123 ist per Definition idempotent, und HTTP-Caches wie Varnish oder ein CDN können die URL direkt als Cache-Key verwenden. GraphQL Response Caching steht vor einem strukturell anderen Problem: Der Standard-Transport ist ein POST-Request mit der Query im Body, und zwei völlig unterschiedliche Anfragen landen technisch auf derselben URL /graphql.
Ein HTTP-Cache, der ausschließlich auf die URL schaut, sieht bei GraphQL-Requests keinen Unterschied zwischen einer Anfrage nach dem Nutzernamen und einer Anfrage nach der kompletten Bestellhistorie, weil beide denselben Endpunkt und dieselbe HTTP-Methode nutzen. Ohne zusätzliche Maßnahmen bleibt GraphQL Response Caching deshalb entweder komplett wirkungslos oder cached versehentlich falsche Antworten für falsche Anfragen.
Die Lösung liegt nicht in einer einzelnen Technik, sondern im Zusammenspiel mehrerer Bausteine: GET-Requests für cachebare Queries, ein deterministischer Cache-Key aus Query und Variablen, und ein Invalidierungsmechanismus, der bei Mutations gezielt nur die betroffenen Cache-Einträge löscht. Die folgenden Abschnitte bauen diese Bausteine Schritt für Schritt auf.
2. GET statt POST: Voraussetzung für HTTP-Caching
Der erste und wichtigste Schritt für GraphQL Response Caching ist der Wechsel von POST zu GET für alle Queries, die keine Mutation sind. HTTP-Caches wie Varnish cachen POST-Antworten standardmäßig gar nicht, weil POST semantisch als nicht-idempotent gilt. GET-Requests dagegen werden von jedem HTTP-Cache automatisch als cachebar behandelt, solange die passenden Cache-Control-Header gesetzt sind.
// graphql-get-client.js — sending cacheable queries as GET requests
async function fetchGraphQLCacheable(query, variables) {
const params = new URLSearchParams({
query,
variables: JSON.stringify(variables ?? {}),
})
const res = await fetch(`/graphql?${params.toString()}`, {
method: 'GET',
headers: { Accept: 'application/json' },
})
return res.json()
}
// Mutations always stay on POST, they are never cacheable
async function runMutation(query, variables) {
const res = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
})
return res.json()
}
Wichtig ist die klare Trennung: Nur reine Query-Operationen wandern auf GET, jede Mutation bleibt konsequent bei POST. Ein weiteres praktisches Problem ist die URL-Länge, komplexe Queries mit vielen Feldern können die von manchen Proxys und Load-Balancern akzeptierte Maximallänge überschreiten, weshalb Persisted Queries, wie in Abschnitt 4 beschrieben, hier oft die bessere Lösung sind.
3. Varnish-VCL für GraphQL-Queries konfigurieren
Varnish braucht explizite VCL-Regeln, um GraphQL-GET-Requests korrekt zu behandeln, weil die Standardkonfiguration Query-Parameter oft ignoriert oder normalisiert, was bei GraphQL zu falschen Cache-Treffern führen kann. Die Grundregel: Der komplette Query-String, der sowohl die Query als auch die Variablen enthält, muss Teil des Cache-Keys sein.
# default.vcl — cache GraphQL GET requests, bypass mutations entirely
vcl 4.1;
sub vcl_recv {
# Only GET requests to /graphql are eligible for caching
if (req.url ~ "^/graphql" && req.method == "GET") {
# Full query string (query + variables) becomes part of the cache key
return (hash);
}
# POST requests (mutations) always bypass the cache
if (req.url ~ "^/graphql" && req.method == "POST") {
return (pass);
}
}
sub vcl_hash {
hash_data(req.url);
if (req.http.Authorization) {
# Separate cache entries per auth context to avoid leaking data across users
hash_data(req.http.Authorization);
}
return (lookup);
}
sub vcl_backend_response {
if (bereq.url ~ "^/graphql") {
# Respect Cache-Control headers set by the GraphQL server
set beresp.ttl = 60s;
set beresp.grace = 300s;
}
}
Der Hash über Authorization ist entscheidend, sobald personalisierte Daten im Spiel sind, sonst könnte ein Nutzer versehentlich die gecachte Antwort eines anderen Nutzers erhalten. Für rein öffentliche, nicht personalisierte Queries kann dieser Header weggelassen werden, um die Cache-Hit-Rate zu erhöhen.
4. Persisted Queries als Cache-Key-Strategie
Persisted Queries lösen gleich zwei Probleme auf einmal: Sie verkürzen lange Query-Strings auf einen kurzen Hash und liefern damit einen deutlich saubereren, kompakteren Cache-Key für GraphQL Response Caching. Statt der vollständigen Query wird nur ein SHA-256-Hash übertragen, den der Server vorab kennt, weil die Query beim Build-Zeitpunkt einmalig registriert wurde.
# Instead of transmitting the full query string as a GET parameter:
# GET /graphql?query={ products(limit: 10) { id title price } }
# Persisted Queries transmit only a short, stable hash:
# GET /graphql?extensions={"persistedQuery":{"version":1,"sha256Hash":"a94a8fe5..."}}
# The hash maps deterministically to this registered query on the server
query ProductList {
products(limit: 10) {
id
title
price
}
}
Für GraphQL Response Caching ist der Vorteil handfest: Ein Hash ist immer gleich lang und enthält keine variablen Whitespace- oder Formatierungsunterschiede, die bei manuell zusammengesetzten Query-Strings zu ungewollten Cache-Misses führen könnten. Zwei Clients, die dieselbe logische Query mit unterschiedlicher Formatierung senden, erzeugen bei Persisted Queries denselben Cache-Key, bei rohen Query-Strings dagegen zwei unterschiedliche.
5. Surrogate Keys für granulare Invalidierung
Eine pauschale TTL von 60 Sekunden reicht für viele Anwendungsfälle nicht aus, wenn sich Daten unvorhersehbar ändern. Surrogate Keys, auch Cache-Tags genannt, lösen dieses Problem, indem jede gecachte Antwort mit den IDs der enthaltenen Entitäten markiert wird. Ändert sich eine einzelne Entität, kann gezielt nur der Cache-Eintrag mit dem passenden Surrogate Key gelöscht werden, statt den kompletten Cache zu leeren.
// surrogate-keys.ts — tagging responses with entity IDs for targeted invalidation
import type { GraphQLResponse } from './types'
function buildSurrogateKeys(response: GraphQLResponse): string {
const keys = new Set<string>()
function collectIds(value: unknown): void {
if (Array.isArray(value)) {
value.forEach(collectIds)
} else if (value && typeof value === 'object') {
const obj = value as Record<string, unknown>
if (obj.__typename && obj.id) {
keys.add(`${obj.__typename}:${obj.id}`)
}
Object.values(obj).forEach(collectIds)
}
}
collectIds(response.data)
return Array.from(keys).join(' ')
}
// Response middleware sets the header before Varnish caches the response
export function setSurrogateKeyHeader(res: { setHeader: Function }, response: GraphQLResponse) {
res.setHeader('Surrogate-Key', buildSurrogateKeys(response))
}
Enthält eine gecachte Antwort beispielsweise die Entitäten Product:42 und Store:7, reicht ein gezielter Purge-Aufruf für Product:42, um genau diesen Eintrag zu invalidieren, ohne den Cache für alle anderen Produkte oder Stores zu leeren. Varnish unterstützt diesen Mechanismus über das xkey-Modul, Fastly hat Surrogate Keys nativ eingebaut.
6. CDN Edge Caching: Fastly, Cloudflare, CloudFront
Während Varnish typischerweise direkt vor dem eigenen Origin-Server läuft, verteilt ein CDN wie Fastly, Cloudflare oder CloudFront gecachte GraphQL-Antworten geografisch über viele Edge-Standorte. Für GraphQL Response Caching bedeutet das kürzere Latenzen für Nutzer, die weit vom Origin-Rechenzentrum entfernt sind, weil Antworten direkt vom nächstgelegenen Edge-Knoten ausgeliefert werden, statt jedes Mal quer über den Globus zu reisen.
Fastly unterstützt Surrogate Keys und Instant Purge nativ und eignet sich deshalb besonders gut für GraphQL Response Caching mit häufigen, granularen Invalidierungen. Cloudflare bietet mit Cache Tags eine ähnliche Funktionalität auf Enterprise-Plänen, während CloudFront eine Kombination aus Cache-Behaviors und Lambda@Edge-Funktionen für individuelle Cache-Key-Logik benötigt. Die Wahl des CDN hängt stark davon ab, wie granular Invalidierung sein muss und wie viele Requests pro Sekunde erwartet werden.
7. Cache-Invalidierung bei Mutations: Purge-Strategien
Jede Mutation, die eine Entität verändert, muss die zugehörigen Cache-Einträge invalidieren, sonst zeigt die API veraltete Daten an, nachdem ein Nutzer bereits eine Änderung vorgenommen hat. Die Kombination aus Surrogate Keys und einem Purge-Aufruf direkt im Mutation-Resolver ist der zuverlässigste Ansatz für konsistentes GraphQL Response Caching.
// mutation-resolver.ts — purge cache entries tied to the mutated entity
async function updateProductPrice(
_parent: unknown,
args: { id: string; price: number },
ctx: { db: Database; cachePurger: CachePurger }
) {
const updated = await ctx.db.products.update(args.id, { price: args.price })
// Purge only the cache entries tagged with this specific product
await ctx.cachePurger.purgeByKey(`Product:${args.id}`)
return updated
}
Diese gezielte Invalidierung ist deutlich effizienter als ein kompletter Cache-Flush nach jeder Mutation, der bei stark frequentierten APIs die Cache-Hit-Rate unnötig ruinieren würde. Für Fastly geschieht der Purge-Aufruf über die REST-API mit dem Surrogate-Key als Parameter, bei Varnish über einen speziellen HTTP-PURGE-Request an den betroffenen Cache-Server.
8. Cache-Hit-Rate messen und Header richtig setzen
Ohne Messung bleibt unklar, ob GraphQL Response Caching tatsächlich wirkt. Varnish liefert über den Header X-Cache: HIT oder X-Cache: MISS direktes Feedback, das sich problemlos in Access-Logs oder ein Monitoring-Dashboard überführen lässt. Eine niedrige Hit-Rate deutet meist auf einen zu kurzen TTL-Wert, eine fehlerhafte Cache-Key-Normalisierung oder zu viele personalisierte Felder in häufig genutzten Queries hin.
Wichtig ist außerdem, dass der GraphQL-Server selbst korrekte Cache-Control-Header pro Query setzt, statt einen einzigen globalen Wert für alle Antworten zu verwenden. Eine Query, die ausschließlich statische Produktdaten liefert, verträgt einen deutlich längeren TTL als eine Query, die aktuelle Lagerbestände enthält. Diese Differenzierung sollte am besten direkt in den Resolvern anhand der angefragten Felder berechnet werden.
9. Edge Caching im Vergleich zu Application-Level-Caching
Varnish und CDN Edge Caching sind nicht die einzige Option für performantere GraphQL-APIs. Application-Level-Caching mit Redis verfolgt einen anderen Ansatz und löst teilweise andere Probleme.
| Ansatz | Latenz-Vorteil | Granularität | Entlastet Origin-Server |
|---|---|---|---|
| Varnish / CDN Edge Caching | Sehr hoch, geografisch verteilt | Ganze Response, per Surrogate Key | Ja, vollständig |
| Redis Application-Level-Cache | Mittel, ein Rechenzentrum | Pro Feld oder Resolver | Teilweise, Server läuft trotzdem |
| In-Memory-Cache im Prozess | Gering, pro Server-Instanz | Pro Resolver-Aufruf | Nein |
In der Praxis ergänzen sich beide Ansätze, statt sich gegenseitig zu ersetzen: Varnish oder ein CDN cachen ganze Responses für nicht personalisierte, häufig abgefragte Queries am Edge, während Redis auf Resolver-Ebene teure Einzelberechnungen oder externe API-Aufrufe zwischenspeichert, die auch bei einem Cache-Miss auf der Response-Ebene weiterhin anfallen würden.
Mironsoft
GraphQL-Performance, Varnish-Konfiguration und CDN-Architektur
Eure GraphQL-API trifft bei jeder Query den Origin-Server?
Wir richten GraphQL Response Caching mit Varnish oder eurem CDN ein, inklusive Persisted Queries, Surrogate Keys und automatischer Invalidierung bei Mutations.
Varnish-Setup
VCL-Konfiguration für GET-basierte GraphQL-Queries und Surrogate Keys
CDN-Integration
Fastly, Cloudflare oder CloudFront für globales Edge Caching anbinden
Invalidierung
Gezielte Purge-Strategien direkt in Mutation-Resolvern einbauen
10. Zusammenfassung
GraphQL Response Caching mit Varnish und CDN Edge Caching überwindet das strukturelle Problem, dass GraphQL standardmäßig über nicht cachebare POST-Requests transportiert wird. GET-Queries schaffen die Grundvoraussetzung für HTTP-Caching, Persisted Queries liefern kompakte, deterministische Cache-Keys, und Surrogate Keys ermöglichen granulare Invalidierung einzelner Entitäten statt kompletter Cache-Flushes.
Der größte Effekt entsteht im Zusammenspiel: Ein CDN wie Fastly verteilt gecachte Antworten geografisch, Varnish übernimmt die Feinsteuerung direkt vor dem Origin-Server, und beide profitieren von präzisen Cache-Control-Headern, die pro Query statt pauschal gesetzt werden. Wer diese Bausteine konsequent kombiniert, reduziert Origin-Last und Latenz spürbar, ohne bei personalisierten Daten Kompromisse bei der Korrektheit einzugehen.
GraphQL Response Caching — Das Wichtigste auf einen Blick
GET statt POST
Grundvoraussetzung für HTTP-Caching, Mutations bleiben konsequent bei POST.
Persisted Queries
Kurze, deterministische Hashes statt langer Query-Strings als Cache-Key.
Surrogate Keys
Gezielte Invalidierung einzelner Entitäten statt komplettem Cache-Flush.
CDN Edge Caching
Geografisch verteilte Antworten senken Latenz für weit entfernte Nutzer.