ESI-Fragmente ohne externen Varnish
Symfony bringt mit der HttpCache-Klasse einen vollwertigen HTTP Reverse Proxy direkt im PHP-Prozess mit, inklusive Unterstützung für Edge Side Includes. Wer diesen Reverse Proxy richtig konfiguriert, cached ganze Seiten und einzelne Fragmente, ohne zusätzliche Infrastruktur wie Varnish betreiben zu müssen.
Inhaltsverzeichnis
- 1. Was der Symfony Reverse Proxy wirklich ist
- 2. HttpCache im Kernel aktivieren
- 3. Cache-Header steuern: public, private, s-maxage
- 4. ESI-Fragmente für teilweise dynamische Seiten
- 5. Surrogate-Keys und gezielte Invalidierung
- 6. Validierungscache mit ETag und Last-Modified
- 7. HttpCache in Produktion: Grenzen und Alternativen
- 8. Trefferquote messen und Cache-Verhalten debuggen
- 9. Reverse Proxy Optionen im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was der Symfony Reverse Proxy wirklich ist
Der Symfony Reverse Proxy ist keine externe Software, sondern eine PHP-Klasse namens HttpCache, die den Kernel umschließt und eingehende Anfragen anhand von HTTP-Cache-Headern beantwortet, bevor die eigentliche Anwendungslogik überhaupt läuft. Das Konzept dahinter folgt exakt der HTTP-Spezifikation: Antworten mit Cache-Control: public und einem gültigen max-age oder s-maxage werden im Reverse Proxy zwischengespeichert und bei identischen nachfolgenden Anfragen direkt ausgeliefert, ohne dass der Symfony-Kernel, die Datenbank oder irgendein Controller involviert wird.
Der entscheidende Vorteil dieses Ansatzes gegenüber einem externen Reverse Proxy wie Varnish liegt in der Einfachheit der Infrastruktur: Es gibt keinen zusätzlichen Dienst, keine separate Konfigurationssprache wie VCL und keine zusätzliche Netzwerk-Hop-Latenz. Für kleinere bis mittelgroße Symfony-Anwendungen, die keine extremen Traffic-Spitzen bedienen müssen, deckt der eingebaute Reverse Proxy einen erheblichen Teil der Caching-Anforderungen ab, ohne dass ein Ops-Team eine zusätzliche Komponente betreiben und überwachen muss.
2. HttpCache im Kernel aktivieren
Um den Symfony Reverse Proxy zu aktivieren, wird die Klasse HttpCache um den Kernel in public/index.php gelegt. Damit fängt HttpCache jede eingehende Anfrage zuerst ab, prüft den eigenen Store auf einen gültigen Cache-Eintrag, und leitet die Anfrage nur bei einem Cache-Miss an den echten Symfony-Kernel weiter. Das Ergebnis wird dann anhand der zurückgegebenen Cache-Header gespeichert, sofern die Antwort als cachebar markiert wurde.
Wichtig für den produktiven Einsatz: Der Default-Store von HttpCache speichert im Dateisystem unter var/cache/prod/http_cache. Bei mehreren PHP-FPM-Workern auf demselben Server ist das unproblematisch, weil sie sich dasselbe Dateisystem teilen. Bei mehreren Servern hinter einem Load Balancer braucht jeder Server einen eigenen, unabhängigen HTTP-Cache, was zu inkonsistenten Trefferquoten führen kann, wenn Nutzer zwischen Servern wechseln.
<?php
// public/index.php — wrap the kernel with Symfony's built-in reverse proxy
declare(strict_types=1);
use App\Kernel;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
use Symfony\Component\HttpKernel\HttpCache\Store;
require_once dirname(__DIR__) . '/vendor/autoload_runtime.php';
return function (array $context) {
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
// Only enable the reverse proxy in prod — dev needs uncached responses
if ($context['APP_ENV'] === 'prod') {
$store = new Store(dirname(__DIR__) . '/var/cache/prod/http_cache');
$kernel = new HttpCache($kernel, $store, null, [
'default_ttl' => 0, // never guess a TTL — require explicit headers
'trace_level' => 'none', // omit X-Symfony-Cache header in production
]);
}
return $kernel;
};
3. Cache-Header steuern: public, private, s-maxage
Damit der Reverse Proxy überhaupt etwas cachen kann, muss jeder Controller explizit angeben, ob und wie lange eine Antwort cachebar ist. Die Response-Methode setPublic() markiert eine Antwort als für alle Nutzer identisch cachebar, während setPrivate() das Gegenteil signalisiert und typischerweise für personalisierte Inhalte genutzt wird. Der Unterschied zwischen max-age und s-maxage ist entscheidend: max-age gilt für Browser-Caches, s-maxage speziell für gemeinsam genutzte Caches wie den Symfony Reverse Proxy oder CDNs.
Eine bewährte Strategie in der Praxis: Produktseiten eines Shops mit s-maxage von wenigen Minuten cachen, während der Browser selbst über ein deutlich kürzeres oder gar kein max-age verfügt. So bleibt der Reverse Proxy die Quelle der Wahrheit für Aktualität, während Endnutzer nie veraltete Inhalte aus ihrem eigenen Browser-Cache sehen.
<?php
// src/Controller/ProductController.php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ProductController extends AbstractController
{
#[Route('/product/{slug}', name: 'product_show')]
public function show(string $slug): Response
{
$product = $this->productRepository->findBySlug($slug);
$response = $this->render('product/show.html.twig', ['product' => $product]);
// Cache in the shared reverse proxy for 5 minutes,
// but let the browser revalidate on every visit.
$response->setPublic();
$response->setSharedMaxAge(300);
$response->headers->addCacheControlDirective('must-revalidate', true);
return $response;
}
}
4. ESI-Fragmente für teilweise dynamische Seiten
Der größte praktische Vorteil des Symfony Reverse Proxy gegenüber einfachem Full-Page-Caching ist die native Unterstützung für Edge Side Includes. Viele Seiten sind größtenteils statisch, enthalten aber einzelne personalisierte Elemente, etwa einen Warenkorb-Zähler im Header. Statt die gesamte Seite nicht cachebar zu machen, rendert man den dynamischen Teil als separates ESI-Fragment mit eigenem Cache-Verhalten, während der Rest der Seite langfristig gecacht bleibt.
Damit ESI funktioniert, muss Esi::enable() im Kernel aktiviert und im Template mit render_esi() statt einem regulären render()-Aufruf gearbeitet werden. Der Reverse Proxy ersetzt beim Ausliefern die ESI-Tags durch das jeweils aktuelle Fragment und cached Hauptseite und Fragment vollständig unabhängig voneinander.
<?php
// src/Controller/CartWidgetController.php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class CartWidgetController extends AbstractController
{
#[Route('/_fragment/cart-widget', name: 'cart_widget')]
public function widget(): Response
{
$response = $this->render('fragment/cart_widget.html.twig', [
'itemCount' => $this->cartService->getItemCount(),
]);
// Short-lived cache — the fragment itself changes per session,
// so this is deliberately private with a tiny shared TTL.
$response->setPrivate();
$response->setMaxAge(0);
return $response;
}
}
5. Surrogate-Keys und gezielte Invalidierung
Ein oft unterschätztes Problem bei jedem Reverse Proxy: Wenn sich ein Produkt ändert, muss die gecachte Seite invalidiert werden, bevor der nächste Nutzer veraltete Daten sieht. Symfonys HttpCache unterstützt dafür das Interface PurgeableInterface, mit dem sich einzelne URLs gezielt aus dem Cache entfernen lassen, sobald ein relevantes Ereignis eintritt, etwa ein Doctrine-Postupdate-Event nach einer Preisänderung.
Für komplexere Abhängigkeiten, bei denen eine Kategorieseite von mehreren Produkten abhängt, reicht eine einzelne URL-Invalidierung oft nicht aus. Hier kommen Surrogate-Keys ins Spiel: Jede Antwort bekommt einen benutzerdefinierten Header mit den IDs aller enthaltenen Entities, und ein Event-Listener sammelt beim Speichern einer Entity alle betroffenen Surrogate-Keys, um gezielt genau die richtigen Cache-Einträge zu invalidieren, ohne den kompletten Cache leeren zu müssen.
6. Validierungscache mit ETag und Last-Modified
Nicht jede Antwort lässt sich mit einem festen max-age cachen, etwa wenn Inhalte sich unregelmäßig ändern und die Aktualität wichtiger ist als eine feste Zeitspanne. Für diesen Fall unterstützt der Symfony Reverse Proxy den Validierungscache über ETag und Last-Modified. Der Controller berechnet einen Hash oder ein Änderungsdatum, und Symfony prüft automatisch, ob der Client bereits eine aktuelle Version besitzt, bevor der komplette Antwortkörper überhaupt gerendert wird.
Der entscheidende Performance-Vorteil: Bei einem 304 Not Modified wird die teure Template-Rendering-Logik komplett übersprungen. Symfonys Response::isNotModified()-Methode übernimmt den Vergleich mit den Request-Headern If-None-Match und If-Modified-Since automatisch, sodass Entwickler nur die Berechnung des ETags selbst implementieren müssen, meist aus einem Hash der relevanten Datenbank-Timestamps.
7. HttpCache in Produktion: Grenzen und Alternativen
So nützlich der eingebaute Reverse Proxy ist, er hat klare Grenzen. Da HttpCache im selben PHP-Prozess läuft wie die Anwendung selbst, teilt er sich Speicher und CPU mit dem eigentlichen Symfony-Kernel, was bei sehr hohem Traffic zum Flaschenhals werden kann. Ein externer Reverse Proxy wie Varnish oder ein CDN wie Cloudflare kann Anfragen beantworten, ohne den PHP-FPM-Worker überhaupt zu belasten, was bei extremen Lastspitzen einen entscheidenden Unterschied macht.
In der Praxis eignet sich der Symfony Reverse Proxy hervorragend für mittelgroße Anwendungen, interne Tools und Projekte, bei denen der Aufwand für zusätzliche Infrastruktur den Nutzen nicht rechtfertigt. Für Enterprise-Shops mit sehr hohem Traffic bleibt die Kombination aus CDN, externem Reverse Proxy und Symfony als Origin-Server die robustere Wahl, wobei die hier beschriebenen Cache-Header trotzdem die Grundlage bilden, auf der auch externe Caches aufbauen.
8. Trefferquote messen und Cache-Verhalten debuggen
Um zu verstehen, ob der Reverse Proxy tatsächlich greift, aktiviert man in der Staging-Umgebung trace_level: full, wodurch Symfony den Header X-Symfony-Cache in jede Antwort schreibt. Dieser Header zeigt exakt, ob eine Anfrage ein Cache-Hit, ein Cache-Miss oder eine Invalidierung war, und für ESI-Fragmente sogar, welches Fragment welchen Status hatte.
In Produktion sollte trace_level auf none stehen, um keine internen Cache-Details preiszugeben, aber während der Entwicklung und in Lasttests ist dieser Header unverzichtbar, um die tatsächliche Trefferquote zu validieren, bevor man sich auf die Performance-Verbesserung durch den Reverse Proxy verlässt.
# Inspect the reverse proxy behavior with curl in staging
curl -sI https://staging.example.com/product/example-slug | grep -i x-symfony-cache
# X-Symfony-Cache: GET /product/example-slug: miss, store
curl -sI https://staging.example.com/product/example-slug | grep -i x-symfony-cache
# X-Symfony-Cache: GET /product/example-slug: fresh
9. Reverse Proxy Optionen im direkten Vergleich
Die Entscheidung zwischen dem eingebauten Symfony Reverse Proxy, einem externen Varnish und einem CDN hängt stark von Traffic-Profil und Infrastruktur-Budget ab. Die folgende Tabelle stellt die drei realistischen Optionen gegenüber.
| Option | Infrastruktur-Aufwand | Entlastet PHP-FPM | Empfehlung |
|---|---|---|---|
| Symfony HttpCache | Keine zusätzliche Infrastruktur | Nein, läuft im selben Prozess | Kleine bis mittlere Anwendungen |
| Varnish | Zusätzlicher Dienst, VCL-Konfiguration | Ja, vollständig | Mittlere bis große Anwendungen |
| CDN (Cloudflare, Fastly) | Externer Dienst, DNS-Umstellung | Ja, vollständig, plus Edge-Nähe | Hoher globaler Traffic |
| CDN + HttpCache kombiniert | CDN als erste Schicht, HttpCache als Fallback | Ja, mit granularer ESI-Logik dahinter | Enterprise-Setups mit ESI-Bedarf |
Ein pragmatischer Mittelweg für wachsende Symfony-Projekte: mit dem eingebauten Reverse Proxy starten, Cache-Header und ESI-Struktur von Anfang an sauber implementieren, und bei Bedarf später ein CDN oder Varnish vorschalten. Die Cache-Header-Logik in den Controllern bleibt dabei unverändert, weil beide Systeme dieselbe HTTP-Cache-Spezifikation respektieren.
Mironsoft
Symfony-Caching-Strategien, HTTP-Performance und CDN-Integration
Jede Anfrage rendert die Seite komplett neu?
Wir richten den Symfony Reverse Proxy produktionsreif ein, implementieren ESI-Fragmente für personalisierte Bereiche und legen bei Bedarf ein CDN sauber davor, für messbar schnellere Antwortzeiten.
Cache-Strategie
Analyse cachebarer Routen und passende Cache-Header pro Endpunkt
ESI-Integration
Personalisierte Fragmente sauber aus dem Full-Page-Cache lösen
Invalidierung
Surrogate-Keys und Event-basierte Cache-Invalidierung implementieren
10. Zusammenfassung
Der Symfony Reverse Proxy in Form der HttpCache-Klasse liefert einen vollwertigen HTTP-Cache direkt im PHP-Prozess, ohne zusätzliche Infrastruktur zu benötigen. Saubere Cache-Header mit setPublic(), setSharedMaxAge() und dem Unterschied zwischen max-age und s-maxage bilden das Fundament. ESI-Fragmente lösen das klassische Problem teilweise dynamischer Seiten, indem personalisierte Bereiche unabhängig vom Rest der Seite gecacht werden.
Surrogate-Keys und Event-basierte Invalidierung sorgen dafür, dass gecachte Inhalte aktuell bleiben, ohne den gesamten Cache bei jeder Änderung zu leeren. Für kleine bis mittlere Symfony-Anwendungen deckt der eingebaute Reverse Proxy die meisten Caching-Anforderungen vollständig ab, während sehr traffic-intensive Projekte zusätzlich ein CDN oder Varnish vorschalten sollten, ohne dabei die grundlegende Cache-Header-Logik in den Controllern zu ändern.
Symfony HTTP Cache Reverse Proxy — Das Wichtigste auf einen Blick
Aktivierung
HttpCache um den Kernel in public/index.php legen, nur in der Produktionsumgebung.
Cache-Header
setPublic() plus setSharedMaxAge() für gemeinsam nutzbare Antworten, setPrivate() für personalisierte Inhalte.
ESI-Fragmente
render_esi() für dynamische Teile, während der Rest der Seite langfristig gecacht bleibt.
Grenzen
Läuft im selben PHP-Prozess wie die Anwendung, bei Extremtraffic zusätzlich CDN oder Varnish erwägen.