Langzeitbelichtung einer Autobahn bei Nacht mit durchgehenden Lichtspuren der Fahrzeuge

20 Sekunden statt 200 Millisekunden: Wie wir den Cache-Miss gefunden haben, der eine Enterprise-Site lahmlegte

"Die Seite ist langsam" ist die unpräziseste Fehlermeldung im Web. Langsam für wen, wo, wann, unter welcher Last? Bei einem Enterprise-Projekt war die Lage allerdings eindeutig: Im kontrollierten Lasttest brachen die Antwortzeiten auf 20 bis 45 Sekunden ein, PHP-Prozesse starben, die Seite war praktisch nicht mehr benutzbar. Im Normalbetrieb: unauffällig, ein paar hundert Millisekunden.

Der Reflex in dieser Situation heißt: mehr Server. Manchmal ist das richtig. Hier wäre es teures Symptom-Doping gewesen, denn das Problem lag nicht bei der Kapazität, sondern bei den Caches, die schlicht nicht griffen. Wie wir das gefunden haben – und was das über Caching-Architekturen lehrt.

Setup: Kontrollierter Lasttest statt Bauchgefühl

Performance-Probleme "durch Draufschauen" zu finden funktioniert nicht. Der erste Schritt war deshalb ein reproduzierbares Szenario: definierte Nutzerpfade, definierte Parallelität, definierte Dauer – und Messpunkte auf jeder Ebene.

Dazu kamen drei Informationsquellen:

Monitoring-Daten aus dem Regelbetrieb: Wo entstehen Antwortzeiten, wie sieht die Verteilung aus (nicht nur der Mittelwert – der lügt bei Ausreißern), wie verhalten sich PHP-Worker, Datenbank und Cache-Layer unter Last?

Code-Review der kritischen Pfade: Welche Requests erzeugen echte Applikationsarbeit? Wo werden Daten pro Request neu berechnet, die eigentlich stabil sind?

Header-Analyse: Was sagen die HTTP-Antworten selbst über ihre Cachebarkeit? Cache-Control, Vary, Set-Cookie – und was meldet Cloudflare als CF-Cache-Status?

Der letzte Punkt brachte den Durchbruch. Reproduzierbarkeit ist bei Performance-Arbeit nicht Bürokratie, sondern die Voraussetzung dafür, hinterher belegen zu können, dass ein Fix wirkt.

Der Befund: Produktseiten waren faktisch nicht cachebar

Die Antwortzeiten unter Last waren nur das Symptom. Die Ursache: Die Produktseiten landeten nie im Cache. Jeder Aufruf – auch identische Aufrufe derselben Seite – ging durch bis in die Applikation und erzeugte dort volle Arbeit: Datenbankabfragen, Rendering, alles.

Drei Mechanismen wirkten zusammen:

Ein Cookie, das alles kippte. Jeder Request bekam ein Cookie gesetzt – harmlos gedacht, fatal in der Wirkung. Sowohl der Edge-Cache als auch der Page Cache behandeln Responses mit Set-Cookie als potenziell nutzerspezifisch und cachen sie nicht. Ein einzelnes überflüssiges Cookie macht die Cachebarkeit einer ganzen Seitenklasse zunichte.

Cache-Control-Header, die zu vorsichtig waren. Auf den Produktseiten stand effektiv "nicht cachen" – ein Erbe aus einer früheren Projektphase, in der die Daten als volatil galten. Inzwischen kamen sie aus einem Sync-Prozess mit klarem Intervall, waren also über Minuten hinweg stabil. Der Header wusste das nur nicht.

Vary-Header mit zu vielen Dimensionen. Jede Dimension im Vary multipliziert die Cache-Varianten. Bei Sprache ist das nötig, bei feingranularen Client-Merkmalen wird daraus eine Cache-Fragmentierung, bei der jede Variante nur einmal abgerufen wird – der Cache läuft leer, bevor er nützt.

Das Ergebnis: Ein Edge-Cache, der fast nur MISS und DYNAMIC meldete – während alle Beteiligten davon ausgingen, dass "der Cache ja steht".

Wo die Invalidierung kippt: Edge, Page Cache, Applikation

Moderne Setups haben mehrere Cache-Ebenen, und jede hat eigene Regeln. Genau in den Übergängen entstehen die Probleme.

Edge (Cloudflare) ist die billigste Ebene: Wird hier beantwortet, sieht euer Server den Request nie. Der Edge respektiert aber Upstream-Header. Wer no-cache oder Set-Cookie sendet, bekommt keinen Edge-Cache – egal wie gut die Cloudflare-Regeln sind.

Page Cache (Applikation) hält fertig gerenderte Seiten. Er ist schneller als Neu-Rendern, aber langsamer als Edge – und er greift ebenfalls nicht bei Cookie- oder no-cache-Responses.

Applikations-Caches (Objekt-, Query-, Render-Caches) reduzieren die Arbeit innerhalb eines Requests. Sie helfen, wenn die oberen Ebenen versagen – aber sie retten nichts, wenn jeder Request bis hierher durchkommt.

Die entscheidende Erkenntnis: Diese Ebenen sind eine Kette, nicht ein Netz. Reißt sie oben, tragen die unteren Ebenen die gesamte Last. Und die Kette reißt fast immer an derselben Stelle: bei den Headern, nicht bei der Cache-Konfiguration. Wer Caching debuggen will, schaut zuerst auf die Response-Header, nicht ins Dashboard des CDN.

 

diagramm-cache-layer

Für Invalidierung gilt der Umkehrschluss: Statt kurzer TTLs, die den Cache dauerhaft schwächen, lieber längere TTLs plus gezieltes Purging, wenn sich Inhalte tatsächlich ändern. Ein Sync-Prozess, der ohnehin weiß, welche Produkte er aktualisiert hat, kann genau diese Seiten invalidieren.

Bot-Traffic: der unterschätzte Cache-Killer

Ein Faktor, der in Performance-Analysen regelmäßig übersehen wird: Wer erzeugt die Last eigentlich?

Ein erheblicher Teil des Traffics kam nicht von Nutzern, sondern von Crawlern und Scrapern. Das wäre unkritisch, wenn diese Requests aus dem Cache beantwortet würden – Bots rufen typischerweise viele Seiten einmal ab, also genau das Muster, für das Caching gemacht ist. Zwei Dinge machten es kritisch:

Bots treffen die Long-Tail. Nutzer bewegen sich auf populären Seiten, deren Cache warm ist. Crawler arbeiten den kompletten Katalog ab – jede Seite ein Cache-Miss, jeder Miss ein voller Applikations-Request. Bei fünfstelligen Produktzahlen ist das ein Dauerlauf gegen die Datenbank.

Challenge-Ausnahmen als Nebeneffekt. Damit legitime Bots (Suchmaschinen, Monitoring, Partner-Integrationen) nicht an Bot-Challenges scheitern, gibt es Ausnahmeregeln. Diese Ausnahmen sind nötig – aber sie führen leicht dazu, dass Requests am Cache vorbei oder mit abweichenden Headern durchlaufen. Dann trifft ausgerechnet der Traffic ungebremst die Applikation, der am einfachsten aus dem Cache zu bedienen wäre.

Praktische Konsequenz: Bot-Management und Caching gehören zusammen betrachtet. Wer sie getrennt konfiguriert – Security-Team hier, Performance dort – produziert genau solche Lücken.

Mitigation und Ergebnis

Die Fixes waren, wie so oft, unspektakulär:

  1. Überflüssiges Cookie entfernt – bzw. auf die Pfade beschränkt, die es wirklich brauchen. Ein Fix, wenige Zeilen, größte Wirkung.
  2. Cache-Header realistisch gesetzt: TTLs passend zum Sync-Intervall der Produktdaten, statt reflexhaftem no-cache.
  3. Vary auf das Nötige reduziert – Sprache ja, alles Weitere nur mit Begründung.
  4. Gezielte Invalidierung statt kurzer TTLs: Der Sync purged die Seiten, die er geändert hat.
  5. Bot-Regeln und Cache-Regeln aufeinander abgestimmt, inklusive Prüfung der Ausnahmen.

Das Ergebnis: Aus 20–45 Sekunden unter Last wurden Antwortzeiten im niedrigen dreistelligen Millisekunden-Bereich, ein Großteil der Requests wird am Edge beantwortet, die PHP-Worker haben Luft. Ohne einen einzigen zusätzlichen Server.

Cache: Gleiche Last, gleiche Hardware – nur Header und Cache-Regeln korrigiert.

Dauerhaft überwachen wir seither nicht nur Antwortzeiten, sondern die Cache-Hit-Rate – aufgeschlüsselt nach Seitentyp. Sie ist der Frühindikator: Sie fällt, sobald ein neues Feature ein Cookie setzt oder einen Header überschreibt. Wochen bevor jemand "die Seite ist langsam" meldet.

Fazit

Performance-Probleme unter Last sind selten Kapazitätsprobleme. Meistens ist es ein Cache, der nicht greift – und der Grund steht in den Response-Headern, nicht im CDN-Dashboard. Der Weg dahin: reproduzierbarer Lasttest, Messpunkte auf jeder Ebene, Header-Analyse, und danach Monitoring der Cache-Hit-Rate statt nur der Ladezeit.

Bricht euer Setup unter Last ein? Wir machen Performance-Audits als Festpreis-Paket: kontrollierter Lasttest, Analyse aller Cache-Ebenen, priorisierte Maßnahmenliste mit Aufwandsschätzung. Ihr wisst danach, woran es liegt – und was der Fix kostet, bevor ihr in Hardware investiert.

Fachlich geprüft von Andreas

Andreas hat an diesem Projekt selbst mitgearbeitet und diesen Artikel auf technische Richtigkeit geprüft. KI-gestützt geschrieben, von Menschen verantwortet.

Zum Profil von Andreas

Wie unsere Blog-Artikel entstehen