DEV Community

Cover image for API Caching mit ETag und Cache-Control: Wie bedingte Anfragen Payloads reduzieren
Emre Demir
Emre Demir

Posted on Originally published at apidog.com

API Caching mit ETag und Cache-Control: Wie bedingte Anfragen Payloads reduzieren

HTTP-Caching für APIs: Cache-Control, ETags und 304 richtig einsetzen

Ihre API sendet wahrscheinlich Tausende Male am Tag dasselbe JSON. Ein Client fragt GET /v1/products/42 an, erhält 18 KB zurück, fragt fünf Minuten später erneut an und bekommt dieselben 18 KB. Nichts hat sich geändert – trotzdem bezahlen Sie für Bandbreite, Serialisierung und Datenbankzugriffe.

Apidog heute ausprobieren

HTTP bietet dafür bereits eine Lösung: Cache-Control legt fest, wie lange eine Antwort frisch bleibt. ETag liefert einen Fingerabdruck, mit dem Clients Änderungen prüfen können. Zusammen verwandeln die Header wiederholte Anfragen in 304 Not Modified-Antworten ohne Body. Zusätzlich schützen ETags Schreibvorgänge vor verlorenen Aktualisierungen.

Wenn Sie bereits den Leitfaden zum Caching von API-Antworten in React kennen, ist dieser Artikel die Serverseite derselben Strategie.

Die drei Schichten des HTTP-Cachings

HTTP-Caching für APIs besteht aus drei getrennten Entscheidungen:

1. Aktualität (Freshness)

Wie lange darf ein Client eine Antwort verwenden, ohne den Server zu fragen?

Cache-Control: max-age=60
Enter fullscreen mode Exit fullscreen mode

Für 60 Sekunden liefert der Client die lokale Kopie aus. Es entsteht kein Netzwerkverkehr. Das ist der günstigste Cache-Hit, aber auch der ungenaueste: Änderungen werden erst nach Ablauf des Timers erkannt.

2. Validierung (Validation)

Ist die Antwort veraltet, muss der Client sie nicht vollständig herunterladen. Er fragt stattdessen:

Hat sich die Ressource geändert?

Dafür sendet er den zuvor erhaltenen Fingerabdruck zurück:

If-None-Match: "33a64df551425fcc55e4d42a148795d9f2"
Enter fullscreen mode Exit fullscreen mode

Ist die Ressource unverändert, antwortet der Server mit 304 Not Modified und ohne Body. ETag und If-None-Match sind die präzise Variante. Last-Modified und If-Modified-Since arbeiten dagegen zeitstempelbasiert und haben nur eine Granularität von einer Sekunde.

3. Invalidierung (Invalidation)

Was passiert mit Kopien, sobald sich die Daten ändern?

Private Client-Caches verfallen über max-age automatisch. Gemeinsam genutzte Caches und CDNs benötigen dagegen explizite Löschungen, kurze TTLs oder Direktiven wie stale-while-revalidate.

Freshness spart am meisten, Validation fängt veraltete Kopien ab und Invalidation hält beide Mechanismen korrekt. Die meisten APIs benötigen alle drei Schichten.

So funktioniert ein 304 Not Modified-Roundtrip

Nehmen wir einen Produkt-Endpunkt als Beispiel.

Erste Anfrage

Der Client hat noch keine Kopie:

GET /v1/products/42 HTTP/1.1
Host: api.example.com
Enter fullscreen mode Exit fullscreen mode

Erste Antwort

Der Server liefert den Body zusammen mit den Cache-Metadaten:

HTTP/1.1 200 OK
Cache-Control: private, max-age=60
ETag: "33a64df551425fcc55e4d42a148795d9f2"
Content-Type: application/json
Content-Length: 18432
Enter fullscreen mode Exit fullscreen mode

Der Client speichert Body und ETag. Für die nächsten 60 Sekunden kontaktiert er den Server nicht.

Revalidierung nach 60 Sekunden

Die Kopie ist veraltet:

GET /v1/products/42 HTTP/1.1
Host: api.example.com
If-None-Match: "33a64df551425fcc55e4d42a148795d9f2"
Enter fullscreen mode Exit fullscreen mode

Der Server vergleicht das eingehende ETag mit dem aktuellen Wert. Stimmen sie überein, antwortet er mit:

HTTP/1.1 304 Not Modified
Cache-Control: private, max-age=60
ETag: "33a64df551425fcc55e4d42a148795d9f2"
Enter fullscreen mode Exit fullscreen mode

Die Antwort enthält keinen Body. Statt 18 KB werden nur wenige hundert Byte Header übertragen. Der Client markiert seine Kopie für weitere 60 Sekunden als frisch und verwendet sie weiter.

Hat sich das Produkt geändert, sendet der Server stattdessen eine normale 200 OK-Antwort mit neuem Body und neuem ETag. Eine 304 ist eine Cache-Anweisung, kein Fehler. Der Erklärer zu 304 Not Modified beschreibt den Statuscode ausführlicher.

Ein bedingter GET benötigt weiterhin einen Roundtrip und muss das aktuelle ETag berechnen. Eingespart werden vor allem die Übertragung der Nutzdaten und das erneute Parsen auf dem Client. Bei großen Listen-Endpunkten können dadurch 60 bis 90 Prozent des API-Egress entfallen.

Wichtige Cache-Control-Direktiven für APIs

Cache-Control bietet viele Direktiven. Für JSON-APIs sind diese fünf besonders relevant.

no-store vs. no-cache

Das ist einer der häufigsten Caching-Fehler:

  • no-store: Die Antwort darf in keinem Cache gespeichert werden.
  • no-cache: Die Antwort darf gespeichert werden, muss aber vor jeder Wiederverwendung revalidiert werden.

Verwenden Sie no-store für wirklich sensible Daten wie Tokens, Bankdaten oder schützenswerte personenbezogene Daten.

no-cache ermöglicht zusammen mit einem ETag weiterhin 304-Antworten. Clients zeigen dabei niemals ungeprüft veraltete Daten an. Wer vorsorglich überall no-store setzt, deaktiviert bedingte Anfragen und bezahlt bei jedem Aufruf die vollständige Nutzdatenübertragung.

private

Cache-Control: private
Enter fullscreen mode Exit fullscreen mode

Die Antwort darf nur im Cache des Endbenutzers gespeichert werden, nicht in gemeinsam genutzten Caches oder CDNs. Antworten, die pro Benutzer variieren – insbesondere authentifizierte API-Antworten –, sollten private verwenden. Andernfalls könnte ein falsch konfigurierter Proxy Kontodaten an einen anderen Benutzer ausliefern.

max-age

max-age definiert die Aktualitätsdauer in Sekunden. Für viele APIs sind 30 bis 300 Sekunden sinnvoll. Ziel ist nicht, Anfragen für einen ganzen Tag zu vermeiden, sondern Bursts und Polling-Schleifen abzufangen.

stale-while-revalidate

Cache-Control: max-age=60, stale-while-revalidate=300
Enter fullscreen mode Exit fullscreen mode

Caches dürfen die veraltete Kopie bis zu fünf weitere Minuten ausliefern und gleichzeitig im Hintergrund aktualisieren. Benutzer erhalten sofort eine Antwort, während der Ursprung kurz darauf aktualisiert wird. CDNs wie Cloudflare und Fastly sowie moderne Browser unterstützen diese Direktive.

Ein sinnvoller Standard für einen authentifizierten Lese-Endpunkt:

Cache-Control: private, max-age=60, stale-while-revalidate=120
ETag: "9f8b2c41aa73e0d5"
Enter fullscreen mode Exit fullscreen mode

Die vollständige Spezifikation steht in RFC 9111, das RFC 7234 als maßgebliches HTTP-Caching-Dokument ersetzt.

Starke und schwache ETags

Das Präfix W/ unterscheidet die beiden ETag-Typen.

Ein starkes ETag verspricht Byte-für-Byte-Gleichheit:

ETag: "33a64df551425fcc"
Enter fullscreen mode Exit fullscreen mode

Zwei Antworten mit demselben starken ETag sind byteweise identisch. Starke ETags eignen sich deshalb für Byte-Range-Anfragen und sind für Parallelitätskontrolle mit If-Match erforderlich.

Ein schwaches ETag verspricht nur semantische Gleichheit:

ETag: W/"33a64df551425fcc"
Enter fullscreen mode Exit fullscreen mode

Die Bytes dürfen sich unterscheiden, etwa durch eine andere Feldreihenfolge oder ein aktualisiertes Zeitstempelfeld. Die Bedeutung der Ressource bleibt jedoch gleich.

Kompressions-Middleware kann ETags beeinflussen. Nginx und manche Frameworks wandeln starke ETags in schwache um, wenn sie Antworten direkt komprimieren. Schlagen Parallelitätsprüfungen hinter einem Proxy unerwartet fehl, prüfen Sie, ob dort ein W/-Präfix hinzugefügt wird.

Verwenden Sie standardmäßig starke ETags, die auf dem unkomprimierten Body basieren. Schwache ETags sollten Sie nur bewusst einsetzen, wenn verschiedene Darstellungen derselben Daten zulässig sind. Weitere Details finden Sie in der MDN-Dokumentation zu ETag.

ETags erzeugen: Body-Hash oder Versionsspalte?

Zwei Strategien sind üblich.

Hash des Antwort-Bodys

Serialisieren Sie die Antwort, hashen Sie sie und setzen Sie den Hash in Anführungszeichen:

ETag: "9f8b2c41aa73e0d5"
Enter fullscreen mode Exit fullscreen mode

MD5 oder SHA-1 sind dafür ausreichend, weil das ETag nur ein Fingerabdruck und keine Sicherheitsgrenze ist.

Vorteil: Die Lösung ist konstruktionsbedingt exakt und benötigt keine Schemaänderung.

Nachteil: Sie erstellen die vollständige Antwort auch für 304-Anfragen. Bandbreite wird gespart, aber nicht unbedingt Serialisierungs- oder Datenbanklast.

Versionsspalte oder updated_at

Leiten Sie das ETag aus günstig verfügbaren Daten ab, zum Beispiel:

ETag: "42-v17"
Enter fullscreen mode Exit fullscreen mode

Dafür kann ein Versionszähler der Zeile oder ein Hash von updated_at verwendet werden. Eine bedingte Anfrage benötigt dann nur einen indizierten Lookup.

Die Version muss allerdings bei jeder Änderung erhöht werden, die die Antwort beeinflusst – auch bei Änderungen in verknüpften Tabellen. Wird eine Änderung übersehen, liefert der Server fälschlicherweise 304 und damit veraltete Daten. Dieser Fehler bleibt oft unsichtbar.

Beginnen Sie mit Body-Hashing. Wechseln Sie bei stark frequentierten Endpunkten zu versionsbasierten ETags, sobald das Profiling zeigt, dass die Serialisierung relevant wird.

Optimistische Parallelität mit If-Match und 412

Dasselbe ETag, das beim Lesen Bandbreite spart, verhindert verlorene Aktualisierungen beim Schreiben.

Das Problem

Zwei Administratoren laden gleichzeitig Produkt 42:

  1. Administrator A ändert den Preis und speichert.
  2. Administrator B korrigiert 30 Sekunden später einen Tippfehler.
  3. Bs veraltete Version überschreibt As Preisänderung.

Ohne Prüfung bleibt der Fehler unbemerkt.

Die Lösung

Der Client bindet die Aktualisierung an die zuletzt gelesene Version:

PUT /v1/products/42 HTTP/1.1
If-Match: "33a64df551425fcc55e4d42a148795d9f2"
Content-Type: application/json
Enter fullscreen mode Exit fullscreen mode

Der Server vergleicht If-Match mit dem aktuellen ETag:

  • Übereinstimmung: Änderung anwenden und 200 OK mit neuem ETag zurückgeben.
  • Keine Übereinstimmung: Mit 412 Precondition Failed ablehnen und die Daten nicht verändern.

Der Client lädt anschließend die aktuelle Version, wendet seine Änderung erneut an und versucht es wieder. Der Leitfaden zu 412 Precondition Failed erklärt diesen Ablauf ausführlicher.

Strikte APIs verlangen If-Match bei jedem PUT und antworten bei fehlendem Header mit 428 Precondition Required. So wird die Prüfung obligatorisch.

Verhalten von CDNs und Proxys

Gemeinsame Caches lesen dieselben Header wie Browser, wenden aber zusätzlich ihre eigenen Regeln an:

  • private schließt eine Antwort vollständig vom CDN-Caching aus.
  • s-maxage=600 setzt eine CDN-spezifische TTL, die von max-age des Browsers abweichen kann.
  • Viele CDNs revalidieren beim Ursprung mit bedingten Anfragen. Antwortet der Ursprung mit 304, aktualisiert das CDN seine Metadaten, ohne den Body erneut abzurufen.
  • Senden Sie Vary korrekt. Liefert eine URL sowohl JSON als auch CSV, benötigen Sie beispielsweise Vary: Accept, damit ein gemeinsamer Cache keine CSV-Antwort an einen JSON-Client ausliefert.
  • Prüfen Sie, ob ein Proxy ETags durch Kompression in schwache ETags umwandelt.

Die relevanten Direktiven sind in der MDN-Dokumentation zu Cache-Control beschrieben.

Express-Beispiel: ETags ausliefern und If-None-Match verarbeiten

Express verwendet standardmäßig schwache ETags. Die manuelle Verarbeitung ermöglicht starke ETags und den 412-Schreibpfad:

import crypto from "node:crypto";
import express from "express";

const app = express();
app.use(express.json());

function etagFor(payload) {
  const hash = crypto.createHash("sha1")
    .update(JSON.stringify(payload))
    .digest("hex");
  return `"${hash}"`;
}

app.get("/v1/products/:id", async (req, res) => {
  const product = await db.products.find(req.params.id);
  const etag = etagFor(product);

  res.set("Cache-Control", "private, max-age=60, stale-while-revalidate=120");
  res.set("ETag", etag);

  if (req.get("If-None-Match") === etag) {
    return res.status(304).end();   // fingerprint matches: no body
  }

  res.json(product);
});

app.put("/v1/products/:id", async (req, res) => {
  const product = await db.products.find(req.params.id);
  const currentEtag = etagFor(product);
  const ifMatch = req.get("If-Match");

  if (!ifMatch) {
    return res.status(428).json({ error: "If-Match header required" });
  }

  if (ifMatch !== currentEtag) {
    return res.status(412).json({
      error: "Resource changed since you fetched it"
    });
  }

  const updated = await db.products.update(req.params.id, req.body);

  res.set("ETag", etagFor(updated));
  res.json(updated);
});
Enter fullscreen mode Exit fullscreen mode

Auch der 304-Zweig sollte Cache-Control und ETag senden. Nach RFC 9111 aktualisiert eine 304 die Metadaten der gespeicherten Antwort. Senden Sie deshalb alle Header erneut, die der Client für die weitere Nutzung seiner Kopie benötigt.

Caching in Apidog überprüfen

Code kann korrekt aussehen und trotzdem durch Middleware oder Proxys falsch cachen. Testen Sie deshalb auf HTTP-Ebene.

Die manuelle Prüfung in Apidog dauert etwa eine Minute:

  1. Senden Sie GET /v1/products/42. Prüfen Sie die Antwort-Header auf ETag und Cache-Control. Das ETag muss in Anführungszeichen stehen. Kopieren Sie den Wert.
  2. Fügen Sie derselben Anfrage den Header If-None-Match mit dem kopierten Wert hinzu und senden Sie sie erneut. Erwartet werden 304 Not Modified und ein leerer Body.
  3. Ändern Sie den Datensatz und senden Sie die Anfrage erneut. Erwartet werden 200 OK und ein neues ETag.

Für Regressionstests können Sie denselben Ablauf automatisieren:

  1. Erste Anfrage senden und das ETag aus den Antwort-Headern in einer Variable speichern.
  2. Zweite Anfrage mit diesem Wert als If-None-Match senden.
  3. Status 304 und einen leeren Body bestätigen.
  4. Einen PUT mit einem absichtlich veralteten Wert wie "deadbeefcafe1234" senden.
  5. Status 412 bestätigen.

Der Leitfaden zu API-Assertions beschreibt die Syntax für Statuscodes und Header. Führen Sie das Szenario in CI aus, damit ein Middleware-Upgrade, das ETags entfernt, sofort als fehlgeschlagene Pipeline auffällt.

Apidog kostenlos herunterladen und den Ablauf für Ihre eigenen Endpunkte einrichten.

FAQ

Was ist der Unterschied zwischen no-cache und no-store?

no-store verbietet das Caching vollständig. Nichts wird auf der Festplatte oder im Speicher abgelegt, und jede Anfrage lädt die vollständige Antwort herunter.

no-cache erlaubt das Speichern, erzwingt aber vor jeder Wiederverwendung eine Revalidierung. Zusammen mit einem ETag sind dadurch weiterhin 304-Antworten und Nutzdateneinsparungen möglich.

Verwenden Sie no-store nur für sensible Daten. Die Direktive überall einzusetzen, ist einer der teuersten Caching-Fehler eines API-Teams.

Funktionieren ETags mit POST?

Meistens nicht – und das ist beabsichtigt. ETags beschreiben den Zustand einer Ressource unter einer URL. POST erstellt normalerweise eine neue Ressource, anstatt einen stabilen Zustand zu lesen. Caches speichern POST-Antworten in der Praxis nicht.

Für Schreibvorgänge sind If-Match bei PUT, PATCH und DELETE relevant. Wenn Sie POST-Antworten cachen möchten, sollte geprüft werden, ob die Operation eigentlich ein GET sein sollte.

Macht eine 304-Antwort meine API schneller?

Sie reduziert die Übertragungsgröße, nicht automatisch die Serverarbeit. Der Server empfängt weiterhin die Anfrage, authentifiziert sie und berechnet das aktuelle ETag. Die CPU-Einsparung hängt davon ab, wie günstig dieser Fingerabdruck erzeugt werden kann.

Die größten Vorteile liegen oft bei Bandbreite, Akkulaufzeit und Renderzeit in langsamen Netzwerken. Messen Sie vorher und nachher. Der Leitfaden zum API-Performance-Testing zeigt, wie Sie Latenz und Durchsatz vergleichen.

Sollte ich ETag oder Last-Modified verwenden?

Wenn möglich, senden Sie beide Header.

ETag ist präziser: Es erkennt Änderungen im Subsekundenbereich und inhaltsbezogene Unterschiede, die ein Zeitstempel übersehen kann. Wenn beide bedingten Header eintreffen, hat If-None-Match Vorrang vor If-Modified-Since.

Last-Modified bleibt als Fallback für ältere Clients und für Caches nützlich, die damit die Aktualität schätzen. Wenn Sie nur einen Header senden, wählen Sie ETag.

Top comments (0)