Ein typisches Szenario aus dem Alltag: Eine Web-App lädt bei jeder Navigation dieselben Ressourcen erneut, obwohl sich Inhalte nur selten ändern. Die Folge sind unnötige Roundtrips, höhere Serverlast und spürbare Latenz. Statt vorschnell mehr CPU oder aggressive Datenbank-Indizes zu planen, lohnt sich oft ein Blick auf korrektes HTTP-Caching. Richtig umgesetzt reduziert es nicht nur Antwortzeiten, sondern stabilisiert auch Systeme unter Last, weil wiederholte Requests zu günstigen „Not Modified“-Antworten werden.
Im Kern geht es darum, Antworten so auszuliefern, dass Clients und Zwischenstationen (Browser, Reverse Proxy, CDN) sie wiederverwenden dürfen, ohne falsche oder veraltete Daten zu zeigen. Dabei helfen drei Bausteine: ETag als Fingerabdruck einer Repräsentation, Cache-Control als Steuerung, wie und wie lange gecacht werden darf, und Conditional Requests (bedingte Requests), um Änderungen effizient zu prüfen.
Wann bringt HTTP-Caching bei APIs wirklich etwas?
Geeignete Endpunkte: stabil, häufig gelesen, selten geschrieben
Besonders dankbar sind GET-Endpunkte mit hohem Leseanteil: Produktkataloge, Konfigurationsdaten, Referenzlisten (Länder, Rollen, Feature-Metadaten) oder „Read Models“ aus CQRS-Setups. Der Nutzen steigt, wenn viele Clients dieselben Ressourcen abrufen und Änderungen relativ selten auftreten.
Ungeeignete Endpunkte: personalisiert, hochdynamisch, nicht deterministisch
Schwierig wird es bei Antworten, die stark nutzerspezifisch sind (z.B. „/me/dashboard“) oder deren Inhalt von Kontext abhängt (A/B-Experimente, Geo-IP, Session-Zustand). Hier ist Caching nicht verboten, aber anspruchsvoller: Der Cache-Key muss Variationen abbilden, und man braucht klare Regeln, welche Header die Repräsentation beeinflussen (z.B. Accept-Language).
Ein häufiger Denkfehler: „Caching = stale“
HTTP-Caching muss keine veralteten Daten bedeuten. Mit bedingten Requests kann ein Client sehr schnell prüfen, ob sich etwas geändert hat. Das ist oft schneller als jede Business-Logik, weil im Idealfall nur Header ausgetauscht werden und die eigentliche Payload entfällt.
ETag sauber einsetzen: FingerabdrĂĽcke statt Ratespiele
Starke vs. schwache ETags verstehen
Ein ETag ist ein Token, das eine konkrete Repräsentation einer Ressource beschreibt. „Stark“ bedeutet: Byte-genau identisch. „Schwach“ bedeutet: semantisch gleichwertig, aber nicht zwingend byte-identisch (z.B. anderes Whitespace). Für APIs ist ein starker ETag häufig sinnvoll, wenn die serialisierte Antwort stabil ist. Ein schwacher ETag kann helfen, wenn Serialisierung nicht deterministisch ist, aber die Daten gleich bleiben.
ETag-Generierung: stabil, kollisionsarm, gĂĽnstig
In der Praxis funktionieren drei Ansätze zuverlässig:
- Version/Revision aus der Datenquelle (z.B. monoton steigende Row-Version, Updated-At mit zusätzlicher Entropie), kombiniert mit Resource-ID.
- Hash der kanonisierten Repräsentation (z.B. JSON mit stabiler Feldreihenfolge). Das ist korrekt, aber kann CPU kosten.
- Domain-Event-Revision: Wenn ein Read Model ĂĽber Events aufgebaut wird, kann die letzte verarbeitete Event-Position als ETag dienen.
Wichtig ist, dass der ETag sich exakt dann ändert, wenn sich die Antwort ändert. Eine ETag-Änderung bei irrelevanten Feldern (z.B. „generatedAt“) zerstört die Wirkung, weil Clients nie ein 304 erhalten.
304 Not Modified richtig nutzen
Clients senden bei Wiederholungsabrufen typischerweise If-None-Match mit dem zuletzt gesehenen ETag. Stimmt er mit dem aktuellen ĂĽberein, antwortet der Server mit 304 und ohne Body. Dadurch sinkt Bandbreite und die Latenz wird stabiler, besonders bei mobilen Netzen.
Cache-Control in der Praxis: Regeln, die Teams durchhalten
Public vs. private: wer darf cachen?
„public“ erlaubt das Caching auch in Shared Caches (Proxy/CDN). „private“ beschränkt es auf den Client (z.B. Browser). Für APIs mit Authentifizierung ist „private“ oft der sichere Standard, wenn Antworten nutzerspezifisch sind. Für anonyme, identische Antworten kann „public“ massiv entlasten.
Max-Age, s-maxage und Revalidierung
max-age definiert, wie lange ein Client eine Antwort ohne Rückfrage verwenden darf. s-maxage gilt für Shared Caches und kann abweichend gesetzt werden (z.B. Browser kurz, CDN länger). Zusätzlich kann Revalidierung erzwungen werden, z.B. wenn Clients zwar cachen dürfen, aber immer prüfen sollen, ob es Updates gibt.
Pragmatische Default-Policy pro Ressourcentyp
Ein bewährtes Muster ist eine einfache Matrix nach Ressourcenkategorie:
| Ressourcentyp | Empfehlung | BegrĂĽndung |
|---|---|---|
| Statische Referenzdaten | public, längere max-age, ETag | Selten Änderungen, hoher Wiederverwendungsgrad |
| Nutzerspezifische Daten | private, kurze max-age, ETag | Shared Caches vermeiden, trotzdem 304 ermöglichen |
| Hochdynamische Feeds | kurze max-age oder keine Speicherung, ggf. nur Revalidierung | Stale-Risiko höher als Nutzen |
| Fehlerantworten | keine oder sehr kurze Caches | Fehler nicht „festschreiben“, Recovery unterstützen |
Typische Stolperfallen: Auth, Variants, Proxies
Authorization-Header und Shared Caches
Viele Setups behandeln Antworten auf Requests mit Authorization-Header konservativ. Das ist sinnvoll, kann aber überraschen: Selbst wenn eine Ressource für viele Nutzer identisch ist, verhindert Auth manchmal das Shared Caching. Hier helfen klare Trennung anonymer und authentifizierter Endpunkte oder Token-unabhängige Cache-Schlüssel, sofern der Inhalt wirklich identisch ist.
Vary-Header korrekt setzen
Wenn sich eine Antwort je nach Request-Header unterscheidet (z.B. Sprache, Kompression, Content-Negotiation), muss Vary das abbilden. Sonst kann ein Cache die falsche Variante ausliefern. In APIs ist das häufig bei Accept, Accept-Language oder Content-Type relevant. Weniger ist hier mehr: nur variieren, wenn wirklich nötig, damit der Cache nicht fragmentiert.
Reverse Proxies und CDNs: Header-Weitergabe testen
Zwischen Client und API liegen oft Gateways, Load Balancer oder CDNs. Diese können Header strippen, normalisieren oder eigene Caching-Regeln anwenden. In der Praxis sollten End-to-End-Tests prüfen, ob ETag, Cache-Control und 304-Verhalten tatsächlich bis zum Client durchreichen. Wenn eine Plattform zusätzliche Schutzmechanismen nutzt, müssen diese mit der API-Policy konsistent sein.
Kleine Umsetzungsschritte, die sofort Wirkung zeigen
Die folgenden Schritte sind bewusst klein gehalten und lassen sich inkrementell in bestehende APIs integrieren, ohne das Datenmodell umzubauen:
- Für jeden GET-Endpunkt festlegen, ob die Antwort global identisch oder nutzerspezifisch ist; daraus „public“ oder „private“ ableiten.
- Pro Ressource einen stabilen ETag definieren (z.B. Revision/Updated-At + ID) und in der Response mitsenden.
- Server-seitig Conditional Requests auswerten: If-None-Match prĂĽfen und bei Match 304 ohne Body senden.
- Cache-Control mit realistischen max-age-Werten starten (kurz beginnen) und anhand von Telemetrie (Cache Hit Rate, Backend-QPS) nachjustieren.
- Varianten identifizieren und Vary nur dort setzen, wo sich die Repräsentation wirklich unterscheidet.
Zusammenspiel mit API-Design und Betrieb
Cache und Versionierung: kompatibel statt ĂĽberraschend
API-Versionierung (z.B. via URL oder Header) sollte Teil des Cache-Keys sein. Sonst können Clients eine alte Repräsentation aus dem Cache bekommen, obwohl sich das Schema geändert hat. Wer Versionierung über Pfade macht (z.B. /v1/…), hat den Cache-Key automatisch getrennt; bei Header-Versionierung braucht es in der Regel Vary.
Fehlerbilder schneller einordnen
Wenn Nutzer „alte Daten“ melden, liegt das häufig an fehlender Revalidierung oder an falsch gesetztem public/private. Bei „Cache wirkt nicht“ sind es oft nicht-stabile ETags oder Responses, die bei jedem Request minimal variieren. Eine schnelle Diagnose ergibt sich aus Header-Vergleich: Ändert sich ETag bei identischem Inhalt? Wird 304 überhaupt erreicht? Werden Cache-Control-Header vom Gateway überschrieben?
Bezug zu Robustheit: Idempotenz und Schutzmechanismen
Caching reduziert Last, löst aber keine Probleme wie übermäßige Request-Raten oder doppelte Verarbeitung. Für eine robuste API-Front ergänzt sich Caching gut mit Rate Limiting für APIs. Außerdem sollte API-Design so ausgelegt sein, dass wiederholte Aufrufe nicht ungewollt Seiteneffekte erzeugen; dazu passt idempotente API-Requests. In komplexeren Backends ist eine saubere Schichtung hilfreich, um Cache-Entscheidungen nicht im Controller-Wust zu verstecken; siehe Service-Layer im Backend.
Sicherheit und Datenschutz: was beim Caching nicht passieren darf
Kein Shared Caching fĂĽr personenbezogene Antworten
Für nutzerspezifische Daten ist „private“ die defensivere Wahl. Zusätzlich sollten Antworten, die personenbezogene Informationen enthalten, nicht versehentlich über gemeinsam genutzte Proxies geteilt werden. Der sicherste Weg ist eine klare Trennung zwischen öffentlichen Ressourcen und solchen, die nur im Kontext einer Identität sinnvoll sind.
Token-Rotation und Logout berĂĽcksichtigen
Wenn ein Client cachen darf, kann er Daten auch dann noch anzeigen, wenn ein Token abläuft oder ein Nutzer sich ausloggt. Das ist nicht automatisch falsch (Offline-Fähigkeit), muss aber zur Produktanforderung passen. Falls nach Logout keine Anzeige mehr erlaubt ist, sollte die Client-App den lokalen Cache aktiv invalidieren oder sensible Antworten gar nicht persistieren.
Was sich in Teams bewährt: wenige Regeln, konsequent umgesetzt
Eine Policy pro Ressourcentyp statt Einzelfall-Debatten
Diskussionen über Sekundenwerte führen selten zu besseren Systemen. Effektiver ist eine begrenzte Anzahl von Profilen („Referenzdaten“, „Profil“, „Feed“) und eine klare Zuordnung in der API-Dokumentation. So bleibt die Cache-Strategie wartbar und reviewbar.
Messbarkeit als Teil der Definition of Done
Wer Caching einführt, sollte es messbar machen: Anteil 304 vs. 200, Backend-Queries pro Request, Latenzen am Gateway. Ohne diese Kennzahlen wird häufig „gefühlt optimiert“, während ETags in der Praxis nie treffen oder Shared Caches durch Vary fragmentiert werden. Technisch sauber heißt hier: verifizierbar im Betrieb.
