Zurück zum Blog
Juli 2026·4 Min. LesezeitKI-generiert
Sprache:DeutschEnglishFrançaisÖsterreichischKlingonisch Originalfassung: English

HTTP-Caching-Header verstehen

#http#performance#web

Die meisten Performance-Probleme, die Leute mit einem CDN oder einer Caching-Schicht lösen wollen, werden eigentlich gratis von drei HTTP-Headern gelöst, richtig gesetzt, die die meisten Apps eher aus Versehen als aus Absicht falsch haben. Cache-Control, ETag und Last-Modified steuern, ob ein Browser eine Ressource neu lädt, günstig neu validiert oder sofort von der Platte ausliefert – und der Unterschied zwischen diesen drei Ergebnissen liegt allein darin, wie man sie konfiguriert.

Cache-Control: der wichtigste Header

Cache-Control sagt dem Browser (und jedem Proxy oder CDN dazwischen), wie lange eine Antwort wiederverwendet werden darf, ohne den Server erneut zu kontaktieren.

Cache-Control: max-age=3600

Das bedeutet: Frag die nächsten 3600 Sekunden nicht einmal beim Server nach – verwende die gecachte Kopie wieder. In diesem Zeitfenster geht überhaupt keine Anfrage raus, was das schnellstmögliche Ergebnis ist, schneller als jede serverseitige Optimierung, weil es keinen Roundtrip gibt.

Gängige Direktiven:

  • max-age=<Sekunden> – wie lange die Antwort aus Sicht eines Browser-Caches frisch ist.
  • public – darf auch von gemeinsam genutzten Caches (CDNs, Proxys) gecacht werden, nicht nur vom Browser.
  • private – nur der Browser darf es cachen; ein gemeinsames CDN vor deiner App darf es nicht.
  • no-cache – trotz des Namens erlaubt das Caching, erzwingt aber bei jeder Verwendung eine Neuvalidierung beim Server (siehe ETag unten). Für „niemals cachen" willst du no-store.
  • no-store – überhaupt nicht cachen, nirgends. Die richtige Wahl für Antworten mit sensiblen nutzerspezifischen Daten.
Cache-Control: private, no-store          # a user's account settings page
Cache-Control: public, max-age=31536000   # a hashed, versioned JS bundle
Cache-Control: no-cache                   # an HTML page you want revalidated every load

Warum statische Assets ein sehr langes max-age verwenden sollten

Gibt deine Build-Pipeline Dateinamen mit Inhalts-Hash aus – app.a3f9c1.js statt app.js –, ändert sich der Dateiname selbst immer dann, wenn sich der Inhalt ändert. Dann ist es sicher, genau diese Datei für immer zu cachen, weil eine neue Version schlicht eine neue URL hat:

Cache-Control: public, max-age=31536000, immutable

immutable ist ein Signal, das weiter geht als max-age: Es sagt dem Browser, diese Ressource nicht einmal bei einem harten Reload neu zu validieren, weil sich der Inhalt unter dieser URL wirklich nicht ändern kann. Das ergibt nur bei gehashten, versionierten Dateinamen Sinn – nie bei einem schlichten app.js, das an Ort und Stelle überschrieben wird, sonst wäre jedes Deployment für bereits geladene Browser unsichtbar.

Neuvalidierung: günstiger als ein vollständiger Neudownload

Für Ressourcen, die sich ändern, aber trotzdem gecacht werden sollen, ist max-age nicht die ganze Geschichte – man möchte auch, dass der Browser günstig prüft „hat sich das tatsächlich geändert?", statt blind neu herunterzuladen. Dafür sind ETag und Last-Modified da.

app.get('/api/report/:id', async (req, res) => {
  const report = await getReport(req.params.id);
  const etag = `"${hashOf(report)}"`;

  res.set('ETag', etag);
  res.set('Cache-Control', 'no-cache');

  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end(); // not modified — no body sent
  }

  res.json(report);
});

Der Browser schickt den erhaltenen ETag bei der nächsten Anfrage über den Header If-None-Match zurück. Passt er noch, antwortet der Server mit 304 Not Modified ganz ohne Body – der Browser verwendet seine gecachte Kopie weiter. Deshalb ist no-cache nicht „nicht cachen": Es heißt „cachen, aber vorher prüfen", und diese Prüfung ist weit günstiger, als die ganze Antwort erneut zu senden.

Last-Modified funktioniert genauso mit einem Zeitstempel statt eines Hashes und wird als If-Modified-Since zurückgesendet:

res.set('Last-Modified', report.updatedAt.toUTCString());

if (req.headers['if-modified-since'] === report.updatedAt.toUTCString()) {
  return res.status(304).end();
}

Bevorzuge ETag, wenn du einen billigen Hash der Antwort berechnen kannst – er ist präzise (jede Inhaltsänderung macht ihn ungültig), während Last-Modified nur sekundengenau ist und davon abhängt, dass deine Daten Änderungszeiten korrekt mitführen.

Kombiniert: eine realistische API-Antwort

Cache-Control: private, max-age=60, must-revalidate
ETag: "9f8c3a"

Das sagt: Verwende die gecachte Kopie bis zu 60 Sekunden, ohne überhaupt zu fragen, und validiere danach mit If-None-Match neu, bevor entweder ein 304 ausgeliefert oder frische Daten geholt werden. Es ist ein guter Standard für authentifizierte API-Antworten, die sich gelegentlich, aber nicht bei jeder Anfrage ändern – der Client bombardiert den Server nicht, hängt aber auch nicht minutenalten Daten fest.

Der Fehler, der „warum aktualisiert sich das nicht"-Bugs verursacht

Cache-Control: public, max-age=31536000 auf einen schlichten, nicht gehashten Dateinamen zu setzen, ist die häufigste Ursache für „ich habe einen Fix deployt und Nutzer sehen weiter die alte Version". Der Browser hat keine Möglichkeit zu wissen, dass sich der Inhalt geändert hat – die URL ist identisch, und du hast ihm ausdrücklich gesagt, ein Jahr lang nicht zu fragen. Die Lösung ist kein kürzeres max-age, sondern der Datei einen Inhalts-Hash im Dateinamen zu geben, sodass eine echte Änderung eine echte neue URL erzeugt, und ein langes max-age ausschließlich für URLs zu reservieren, die so funktionieren.

Fazit

Cache-Control entscheidet, ob der Browser überhaupt eine Anfrage stellt. ETag/Last-Modified entscheiden, wenn er fragt, ob der Server mit einem nahezu kostenlosen 304 antworten kann, statt den Body erneut zu senden. Nutze langes, unveränderliches Caching nur bei inhaltsgehashten URLs; nutze kurzes max-age plus Neuvalidierung für alles, was sich unter einer stabilen URL ändern kann; und nutze no-store für alles Sensible, das nie in einen Cache geschrieben werden soll.

NeuerReflows und Repaints: Warum sich deine Seite ruckelig anfühltÄlterEin praktischer Leitfaden zur API-Pagination