cache_id, private Content und Cache Tags von innen betrachtet
Magentos Full Page Cache entscheidet in Millisekunden über ganze Seitenladezeiten, doch die meisten Entwickler kennen nur die Symptome von Cache-Missen, nicht die Mechanik dahinter. Dieser Artikel erklärt cache_id-Generierung, private Content, Cache-Tag-Invalidierung und systematisches Debugging mit X-Magento-Cache-Debug, unabhängig davon ob Varnish oder der eingebaute Cache im Einsatz ist.
Inhaltsverzeichnis
- 1. Magentos Full Page Cache im Überblick: Built-in vs. Varnish
- 2. Request-Lifecycle: Wie eine Seite überhaupt in den Cache gelangt
- 3. Cache-ID-Generierung: Wie Magento eine Seite eindeutig identifiziert
- 4. Private Content: Blöcke mit cacheable=false und Customer Data
- 5. Cache Tags: Wie Invalidierung beim Speichern funktioniert
- 6. Varnish als Backend: Bans und die Rolle von Magento
- 7. X-Magento-Cache-Debug: Cache-Treffer und -Fehlschläge sichtbar machen
- 8. Eigene Cache Tags: IdentityInterface, Plugins und Observer
- 9. Systematisches Debugging von Cache-Misses
- 10. Zusammenfassung
- 11. FAQ
1. Magentos Full Page Cache im Überblick: Built-in vs. Varnish
Der Full Page Cache (FPC) von Magento speichert das vollständig gerenderte HTML einer Seite als fertige HTTP-Antwort und liefert sie bei nachfolgenden Requests aus, ohne den kompletten Layout- und Block-Rendering-Prozess erneut zu durchlaufen. Magento kennt dafür zwei Betriebsmodi: den eingebauten Cache, der die Antwort über das normale Cache-Backend (Redis, Datei, Memcached) im PHP-Prozess speichert und ausliefert, und die Varnish-Integration, bei der ein vorgeschalteter Reverse Proxy die Antwort komplett außerhalb von PHP hält.
Der Unterschied ist mehr als graduell. Beim eingebauten Cache muss PHP-FPM trotzdem bei jedem Request gestartet werden, den Bootstrap durchlaufen und die Cache-Lookup-Logik ausführen, bevor die gespeicherte Antwort zurückgegeben wird. Das spart das teure Rendering, aber nicht den PHP-Overhead selbst. Varnish dagegen beantwortet einen Cache-Hit direkt im Reverse Proxy, ganz ohne PHP-FPM-Aufruf, was in der Praxis Antwortzeiten von unter zehn Millisekunden ermöglicht und bei Lastspitzen deutlich mehr Kapazität freihält. Magento empfiehlt Varnish deshalb explizit für Produktivsysteme, während der eingebaute Cache vor allem für kleinere Shops oder Staging-Umgebungen ohne zusätzliche Infrastruktur ausreicht.
2. Request-Lifecycle: Wie eine Seite überhaupt in den Cache gelangt
Der Cache-Lookup passiert frühzeitig im Request-Zyklus, noch bevor der komplette Layout-Baum aufgebaut wird. Zuständig ist Magento\Framework\App\PageCache\Kernel: Dessen Methode load() berechnet aus dem Request eine Cache-ID und prüft, ob unter diesem Schlüssel bereits eine Antwort im Cache-Backend liegt. Ist das der Fall, wird die gespeicherte Antwort direkt zurückgegeben und der Rest der Applikation, inklusive Layout-Rendering und Block-Aufbau, wird komplett übersprungen. Genau das ist der Performance-Gewinn: Nicht schnelleres Rendering, sondern gar kein Rendering.
Bei einem Cache-Miss durchläuft der Request die volle Magento-Pipeline. Am Ende entscheidet die Response selbst, ob sie cachefähig ist: Über Cache-Control-Header und den X-Magento-Tags-Header, den Kernel::process() nach dem Rendering auswertet. Nur Antworten mit Cache-Control: public und einer positiven TTL werden tatsächlich gespeichert. Controller-Actions, die bewusst setNoCacheable() aufrufen oder eine aktive Kundensession voraussetzen, werden nie im FPC landen, unabhängig davon wie oft sie aufgerufen werden.
3. Cache-ID-Generierung: Wie Magento eine Seite eindeutig identifiziert
Die Cache-ID ist kein einfacher URL-Hash. Magento\Framework\App\PageCache\Identifier baut den Schlüssel aus mehreren Komponenten zusammen: der vollständigen Request-URL inklusive Query-String, der Store-View, dem Schema (http vs. https) und einem sogenannten Vary-Wert. Dieser Vary-Wert stammt aus Magento\Framework\App\Http\Context und enthält alle Werte, die den HTML-Output beeinflussen könnten, ohne Teil der URL zu sein: Kundengruppe, Währung, aktive Preisregeln und alle zusätzlichen Werte, die Module selbst per Plugin in den Context einspeisen.
Genau hier entstehen die häufigsten Überraschungen. Jeder zusätzliche Wert im Http\Context multipliziert die Anzahl der möglichen Cache-Varianten für dieselbe URL. Ein Modul, das etwa das Ergebnis eines A/B-Tests oder eine Geolocation-Erkennung in den Context schreibt, sorgt dafür, dass dieselbe Produktseite für verschiedene Besucher unter verschiedenen Cache-IDs gespeichert wird, und die effektive Trefferquote sinkt. Der Vary-Wert wird zusätzlich als Base64-codiertes Cookie X-Magento-Vary an den Client gesendet, damit Varnish denselben Schlüssel ohne PHP-Aufruf berechnen kann.
<?php
declare(strict_types=1);
namespace Mironsoft\Pricing\Plugin;
use Magento\Framework\App\Http\Context;
/**
* Adds a custom context value that influences the FPC cache_id.
* Every additional context key increases the number of stored
* page variants for the same URL, so this should be used sparingly.
*/
class AddCustomerSegmentToContext
{
private const CONTEXT_SEGMENT = 'customer_segment';
/**
* Injects the resolved customer segment into the HTTP context.
*
* @param Context $subject Original HTTP context instance.
* @return void
*/
public function beforeGetVaryString(Context $subject): void
{
if (!$subject->hasData(self::CONTEXT_SEGMENT)) {
// Cheap default: avoid computing an expensive segment on every request
$subject->setValue(self::CONTEXT_SEGMENT, 'default', 'default');
}
}
}
4. Private Content: Blöcke mit cacheable=false und Customer Data
Nicht jeder Teil einer Seite darf für alle Besucher identisch aus dem Cache kommen. Mini-Cart, Begrüßung mit Kundennamen und Wunschlisten-Zähler sind Private Content: Blockdaten, die pro Besucher unterschiedlich sind, obwohl der Rest der Seite für alle gleich aussieht. Im Layout-XML markiert man solche Blöcke mit cacheable="false". Beim eingebauten FPC bedeutet das: Der Block wird beim Rendern durch einen leeren Platzhalter ersetzt und die gesamte Seite bleibt trotzdem cachefähig.
Gefüllt wird der Platzhalter erst im Browser, über Customer Data (Section.js): Ein AJAX-Call an customer/section/load lädt die privaten Daten nach dem initialen Rendering nach und Alpine.js beziehungsweise das Section-Data-Binding ersetzt den Platzhalter clientseitig. Dieses Muster entkoppelt Cache-Fähigkeit vollständig von Personalisierung: Die Seite selbst bleibt für jeden Besucher identisch cachebar, nur die kleinen dynamischen Fragmente werden asynchron nachgeladen. Bei Varnish kann derselbe Effekt zusätzlich über ESI (Edge Side Includes) direkt im Reverse Proxy erreicht werden, ohne einen zusätzlichen Client-Request.
<!-- Layout XML: Block explizit als nicht cachefaehig markieren -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="header.panel">
<!-- cacheable="false" ersetzt den Block-Output durch einen
leeren Platzhalter, der ueber Customer Data (Section.js)
nach dem Laden clientseitig gefuellt wird -->
<block class="Mironsoft\Loyalty\Block\PointsBalance"
name="loyalty.points.balance"
template="Mironsoft_Loyalty::points-balance.phtml"
cacheable="false"/>
</referenceContainer>
</body>
</page>
5. Cache Tags: Wie Invalidierung beim Speichern funktioniert
Eine gecachte Produktseite bleibt nicht ewig gültig, sie muss geleert werden, sobald sich das zugrunde liegende Produkt ändert. Dafür sammelt Magento während des Renderings alle Cache Tags der beteiligten Entitäten: Jedes Modell, das Magento\Framework\DataObject\IdentityInterface implementiert, liefert über getIdentities() eine Liste von Tags wie cat_p_123 für Produkt 123 oder cat_c_45 für Kategorie 45. Alle Tags aller Blöcke einer Seite landen gesammelt im X-Magento-Tags-Header der Response und werden zusammen mit dem HTML im Cache-Eintrag gespeichert.
Beim Speichern einer Entität löst Magento über das Event clean_cache_by_tags die Invalidierung aus: Magento\Framework\App\CacheInterface::clean() wird mit genau den Tags aufgerufen, die das gespeicherte Objekt liefert. Das Cache-Backend entfernt daraufhin gezielt nur die Einträge, die diesen Tag enthalten, nicht den kompletten Cache. Bei Redis passiert das über einen Tag-Index, bei Varnish über einen BAN-Request mit Regex auf den Tag. Genau diese Granularität macht den Unterschied zwischen einer Preisänderung, die eine einzelne Produktseite invalidiert, und einem kompletten Cache-Flush, der jede gecachte Seite auf einmal verwirft.
{
"_comment": "Debug view: aggregated cache tags for one rendered page",
"url": "/catalog/product/view/id/123",
"cache_id": "e4a1c9...b02f",
"x_magento_tags": [
"cat_p_123",
"cat_p",
"cat_c_45",
"cat_c",
"block_html_price_123",
"FPC"
],
"note": "Saving product 123 triggers clean_cache_by_tags with cat_p_123, invalidating only entries that carry this tag"
}
6. Varnish als Backend: Bans und die Rolle von Magento
Auch wenn Varnish die Antwort ausliefert, bleibt die Entscheidungslogik in Magento. Varnish selbst weiss nichts von Produkten, Kategorien oder Preisregeln, es kennt nur HTTP-Header. Magento sendet deshalb beim Invalidieren keinen einfachen Cache-Flush, sondern einen BAN-Request über Magento\PageCache\Model\Varnish\ProxyList an jeden konfigurierten Varnish-Host, mit dem gesammelten Tag als Regex im Header X-Magento-Tags-Pattern. Varnish vergleicht diesen Pattern intern gegen den gespeicherten X-Magento-Tags-Header jedes Cache-Eintrags und verwirft nur die Treffer.
Wichtig für den Betrieb: Wenn ein Varnish-Host in der Liste nicht erreichbar ist, meldet Magento das nur als Log-Eintrag, der Request selbst schlägt nicht fehl. Bei Multi-Server-Setups mit mehreren Varnish-Instanzen hinter einem Load Balancer müssen deshalb alle Hosts korrekt in varnish.host respektive der Systemkonfiguration hinterlegt sein, sonst bleibt ein Teil der Instanzen mit veralteten Daten stehen, während andere bereits invalidiert wurden. Ein klassisches Symptom dafür: Ein Preis wird aktualisiert, ist aber je nach Lastverteilung mal alt, mal neu sichtbar.
7. X-Magento-Cache-Debug: Cache-Treffer und -Fehlschläge sichtbar machen
Der X-Magento-Cache-Debug-Header ist der schnellste Weg, um zwischen HIT und MISS zu unterscheiden, ohne Server-Logs durchsuchen zu müssen. Aktiviert wird er über Varnishs default.vcl beziehungsweise, beim eingebauten Cache, automatisch durch Magento\Framework\App\Response\Http\Interceptor. Ein HIT bedeutet, die Antwort kam direkt aus dem Cache-Backend ohne erneutes Rendering, ein MISS bedeutet, die komplette Magento-Pipeline wurde durchlaufen.
In der Praxis reicht ein einfacher curl-Aufruf, um systematisch zu prüfen, ob eine Seite überhaupt cachefähig ist, und ob wiederholte Aufrufe tatsächlich zu einem HIT führen. Bleibt eine Seite dauerhaft bei MISS, obwohl sie eigentlich cachefähig sein sollte, deutet das auf ein Vary-Problem, einen fehlenden oder falschen Cache-Control-Header oder eine Session zurück, die bei jedem Request neu gestartet wird.
#!/usr/bin/env bash
# Check FPC hit/miss status for a product page, two requests in sequence
URL="https://shop.example.com/catalog/product/view/id/123"
echo "First request (expected MISS on cold cache):"
curl -sI "$URL" | grep -i -E "x-magento-cache-debug|cache-control|age"
echo ""
echo "Second request (expected HIT):"
curl -sI "$URL" | grep -i -E "x-magento-cache-debug|cache-control|age"
# Compare the X-Magento-Vary cookie across requests with different sessions
echo ""
echo "Vary cookie check:"
curl -sI -H "Cookie: X-Magento-Vary=" "$URL" | grep -i set-cookie
8. Eigene Cache Tags: IdentityInterface, Plugins und Observer
Für eigene Module reicht es meist nicht, sich auf die eingebauten Tags von Produkt und Kategorie zu verlassen. Ein eigenes Modell, dessen Änderung eine gecachte Seite beeinflusst, etwa ein Loyalty-Punktestand oder ein individueller Rabatt, sollte selbst IdentityInterface implementieren und über getIdentities() ein eigenes Tag-Präfix liefern. Damit wird das Modell Teil derselben Invalidierungs-Pipeline wie Produkte und Kategorien, ohne dass man die Cache-Logik manuell nachbauen muss.
Für Fälle, in denen kein Standard-Save-Event greift, etwa bei Massenupdates über direkte SQL-Statements oder externe Importe, kann die Invalidierung auch explizit über einen Plugin auf das ResourceModel ausgelöst werden, der CacheInterface::clean() direkt mit den passenden Tags aufruft. Wichtig ist, die Tag-Namen konsistent zu halten: Der gleiche Tag, der beim Rendern über getIdentities() gesetzt wird, muss beim Speichern exakt so wieder aufgerufen werden, sonst verwaist der Cache-Eintrag und wird nie invalidiert.
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Model;
use Magento\Framework\DataObject\IdentityInterface;
use Magento\Framework\Model\AbstractModel;
/**
* Loyalty points balance model, participates in FPC tag invalidation.
*/
class PointsBalance extends AbstractModel implements IdentityInterface
{
private const CACHE_TAG = 'mironsoft_loyalty_points';
/**
* Returns cache tags identifying this entity for FPC invalidation.
*
* @return string[]
*/
public function getIdentities(): array
{
return [self::CACHE_TAG . '_' . $this->getCustomerId()];
}
}
/**
* Plugin on the resource model: explicit invalidation for bulk updates
* that bypass the standard save() event (e.g. direct SQL imports).
*/
class InvalidateCacheOnBulkUpdate
{
public function __construct(
private readonly \Magento\Framework\App\CacheInterface $cache
) {
}
/**
* Cleans FPC entries for affected customers after a bulk update.
*
* @param mixed $subject Bulk update resource.
* @param mixed $result Original method result.
* @param array $customerIds Affected customer IDs.
* @return mixed
*/
public function afterBulkUpdate($subject, $result, array $customerIds)
{
$tags = array_map(
static fn (int $id): string => 'mironsoft_loyalty_points_' . $id,
$customerIds
);
$this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, $tags);
return $result;
}
}
9. Systematisches Debugging von Cache-Misses
Wer bei jedem einzelnen Symptom neu rät, verliert Zeit. Ein wiederholbarer Ablauf spart Debugging-Runden: Zuerst prüfen, ob der FPC überhaupt aktiviert ist, unter Admin, System, Cache Management, Status Page Cache. Danach den X-Magento-Cache-Debug-Header mehrfach hintereinander abfragen, um zu sehen, ob überhaupt jemals ein HIT entsteht. Bleibt es konstant bei MISS, ist meist ein Cache-Control: no-cache oder ein fehlender TTL-Wert die Ursache, oft ausgelöst durch einen Controller, der setNoCacheable() aufruft, oder durch eine Session, die bei jedem Request neu gestartet wird, etwa weil ein Modul im Konstruktor eines Blocks auf CustomerSession zugreift.
Wechseln HIT und MISS unregelmäßig, liegt es meist an der Cache-ID selbst: Ein zusätzlicher Wert im Http\Context, ein A/B-Testing-Cookie oder eine Geolocation-Erkennung erzeugt mehr Varianten, als tatsächlich nötig sind. bin/magento cache:status zeigt den Aktivierungsstatus, varnishlog beziehungsweise varnishstat zeigen bei Varnish live, welche Requests als HIT, MISS oder PASS behandelt werden. Ein häufig übersehener Sonderfall: Entwickler-Browser mit aktivem Cookie für eingeloggte Admin-Vorschau erzwingen über die X-Magento-Vary-Logik dauerhaft einen eigenen, meist nicht wiederverwendeten Cache-Eintrag, was in Monitoring-Dashboards fälschlich wie eine niedrige Trefferquote aussieht.
| Symptom | Falsche Annahme | Tatsächliche Ursache | Lösung |
|---|---|---|---|
| Dauerhaft MISS | Cache-Backend ist kaputt | Controller ruft setNoCacheable() auf | Cache-Control-Header prüfen |
| Niedrige Trefferquote | Zu kurze TTL | Zu viele Werte im Http\Context | Vary-Werte auf das Nötigste reduzieren |
| Alte Preise sichtbar | Cache-Flush war nötig | Fehlendes Tag bei Bulk-Import | IdentityInterface + Plugin-Invalidierung |
| Private Daten für alle sichtbar | Zufällige Race Condition | Block ohne cacheable=false gerendert | Layout-XML korrigieren |
| Ungleiche Werte je Server | Datenbank-Replikations-Lag | Varnish-Host in ProxyList fehlt | Alle Hosts in varnish.host eintragen |
Mironsoft
Full-Page-Cache-Architektur, Varnish-Konfiguration und Performance-Engineering für Magento
Cache-Misses zuverlässig unter Kontrolle bringen?
Wir analysieren die Cache-Hit-Rate eures Magento-Shops, identifizieren Vary-Probleme und fehlende Cache Tags und implementieren eine saubere FPC-Architektur, egal ob eingebauter Cache oder Varnish im Einsatz ist.
Cache-Audit
Hit-Rate-Analyse, Vary-Kontext prüfen, X-Magento-Cache-Debug auswerten
Tag-Invalidierung
IdentityInterface für Custom-Module, granulare Invalidierung statt Full-Flush
Varnish-Setup
Multi-Host-Konfiguration, ProxyList-Absicherung und Monitoring aufbauen
10. Zusammenfassung
Die Full-Page-Cache-Architektur in Magento löst ein zentrales Problem: Ganze Seiten müssen nicht bei jedem Request neu gerendert werden. Magento\Framework\App\PageCache\Kernel entscheidet früh im Request-Zyklus, ob eine gespeicherte Antwort ausgeliefert werden kann. Die Cache-ID kombiniert URL, Store und einen Vary-Wert aus Http\Context, wobei jeder zusätzliche Context-Wert die Trefferquote senkt. Private Content über cacheable="false" und Customer Data trennt Personalisierung sauber von Cachefähigkeit. Cache Tags über IdentityInterface sorgen für granulare Invalidierung statt kompletter Cache-Fluts bei jeder Änderung.
Der entscheidende Hebel liegt selten in einer einzelnen Einstellung, sondern im Zusammenspiel aller Mechanismen: Ein korrekt gesetzter Cache-Tag nützt nichts, wenn ein Controller die Seite ohnehin per setNoCacheable() von der Speicherung ausschliesst. Wer X-Magento-Cache-Debug konsequent in die eigene Debugging-Routine integriert und bei jedem neuen Modul prüft, ob es unnötig den Http\Context erweitert oder Tags vergisst, verhindert die schleichende Erosion der Cache-Hit-Rate, die viele Magento-Shops nach Monaten im Betrieb betrifft.
Full-Page-Cache-Architektur in Magento, Das Wichtigste auf einen Blick
Cache-ID
URL, Store und Http\Context-Vary-Werte bestimmen den Schlüssel. Jeder zusätzliche Context-Wert erzeugt mehr Cache-Varianten.
Private Content
cacheable="false" plus Customer Data (Section.js) trennt personalisierte Fragmente von der cachefähigen Seite.
Cache Tags
IdentityInterface::getIdentities() plus clean_cache_by_tags ermöglichen granulare statt vollständige Invalidierung.
Debugging
X-Magento-Cache-Debug per curl prüfen, Http\Context minimal halten, alle Varnish-Hosts korrekt konfigurieren.