- 11. Mai 2026
#lessonlearned – Edge-Caching mit Cloudflare Workers: Inhalte gezielt cachen und aktuell halten
Wenn eine Nuxt-Anwendung auf Cloudflare Workers betrieben wird, funktionieren klassische Caching-Ansätze nicht mehr wie in einer herkömmlichen Serverumgebung. Gerade bei einer SSR-Anwendung mit vielen Seiten und externen CMS-Abfragen kann das schnell zu unnötiger Last und längeren Antwortzeiten führen. In diesem Lesson Learned zeigen wir, wie wir Seiteninhalte direkt an der Edge zwischenspeichern, mit unterschiedlichen Laufzeiten versehen und bei Veröffentlichungen in Storyblok gezielt wieder aus dem Cache entfernen.

Warum ist Edge-Caching in unserem Fall so wichtig?
Unsere Nuxt-4-Anwendung bedient mehr als 30 Websites über eine gemeinsame Codebasis. Ohne Caching würde bei jedem einzelnen Request der komplette Server-Side-Rendering-Prozess erneut ausgeführt werden. Dazu gehören unter anderem API-Abfragen an Storyblok und das anschließende Rendern der Seite.
Das ist technisch zwar korrekt, aber nicht effizient. Denn viele Inhalte ändern sich nur selten. Trotzdem würde dieselbe Seite immer wieder neu aufgebaut werden. Mit zunehmendem Traffic führt das zu vermeidbarer Last und bremst die Auslieferung unnötig aus.
Da wir auf Cloudflare Workers setzen, haben wir das Caching direkt an die Edge verlagert und dafür die Cloudflare Cache API genutzt.
Unser Ansatz: Caching in zwei Nitro-Plugins aufteilen
Damit die Logik übersichtlich bleibt, haben wir das Caching in zwei getrennte Nitro-Plugins aufgeteilt:
Ein Plugin prüft beim Request, ob bereits eine fertige Version im Cache liegt
Das zweite Plugin speichert die erzeugte Antwort nach der Auslieferung im Cache
Diese Trennung hat sich für uns als sehr sinnvoll erwiesen, weil Lesen und Schreiben damit klar voneinander abgegrenzt sind.
1. Cache beim Request prüfen
Im ersten Schritt wird bei jedem eingehenden Request geprüft, ob die Seite bereits im Cloudflare-Cache vorhanden ist. Dabei cachen wir nur Inhalte, die sich dafür wirklich eignen:
nur
GET-Requestskeine API-Routen
keine statischen Assets
keine Inhalte im Entwurfsmodus
Ein vereinfachter Ausschnitt sieht so aus:
// server/plugins/cache-check.ts
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', async (event) => {
if (event.node.req.method !== 'GET') return
if (url.startsWith('/_nuxt/') || url.startsWith('/api/')) return
if (storyblokVersion !== 'published') return
const cache = caches.default
const cacheKey = new Request(url, { method: 'GET' })
const cachedResponse = await cache.match(cacheKey)
if (cachedResponse) {
event.node.res.setHeader('X-Cache-Status', 'HIT')
event.node.res.end(await cachedResponse.text())
}
})
})Der entscheidende Punkt ist hier: Wenn ein Cache-Treffer vorliegt, wird die Antwort sofort zurückgegeben. Der eigentliche SSR-Prozess wird dann komplett übersprungen. Genau das spart Rechenzeit und verbessert die Antwortgeschwindigkeit spürbar.
2. Antwort nach der Auslieferung im Cache speichern
Wenn keine gecachte Version gefunden wurde, wird die Seite regulär gerendert. Anschließend speichern wir die fertige Antwort im zweiten Plugin im Cache ab.
Dabei vergeben wir die Gültigkeitsdauer nicht pauschal, sondern abhängig vom Inhaltstyp. So können wir häufiger wechselnde Inhalte kürzer cachen als statische Seiten.
// server/plugins/cache-store.ts
nitroApp.hooks.hook('afterResponse', async (event) => {
const ttlMap = {
news: 1800,
careers: 1800,
events: 3600,
default: 86400
}
const headers = new Headers({
'Content-Type': 'text/html',
'Cache-Control': `public, s-maxage=${ttl}, stale-while-revalidate=86400`,
'Cache-Tag': `storyId-${storyId}`
})
event.context.cloudflare.context.waitUntil(
cache.put(cacheKey, new Response(body, { headers }))
)
})News und Karriereseiten erhalten in unserem Fall zum Beispiel eine kürzere Laufzeit als klassische Inhaltsseiten. So bleibt das Caching flexibel und passt besser zum tatsächlichen Änderungsverhalten der Inhalte.
Besonders wichtig ist hier waitUntil(). Damit läuft die Speicherung im Cache im Hintergrund weiter, ohne die Antwort an den Benutzer zu verzögern.
Veröffentlichungen in Storyblok gezielt berücksichtigen
Ein Cache ist nur dann wirklich hilfreich, wenn neue Inhalte auch zeitnah sichtbar werden. Deshalb leeren wir nicht pauschal den gesamten Cache, sondern nur die betroffenen Seiten.
Sobald Inhalte in Storyblok veröffentlicht werden, löst ein Webhook einen spezialisierten Worker aus. Dieser entfernt gezielt die Einträge mit dem passenden Cache-Tag.
const purgeBody = JSON.stringify({ tags: [`storyId-${data.story_id}`] })
await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`, {
method: 'POST',
body: purgeBody
})Durch den Cache-Tag-Header können wir einzelne Seiten sehr gezielt invalidieren. Das hat den Vorteil, dass nicht unnötig der komplette Cache geleert werden muss. Bereits gecachte Seiten, die sich nicht geändert haben, bleiben weiterhin schnell auslieferbar.
Was sich in der Praxis bewährt hat
Im Projekt haben sich vor allem drei Punkte als besonders hilfreich erwiesen.
Unterschiedliche Cache-Laufzeiten pro Inhaltstyp
Nicht jede Seite muss gleich behandelt werden. News, Jobs oder Veranstaltungen ändern sich deutlich häufiger als Standardseiten. Eine inhaltsabhängige Gültigkeitsdauer sorgt deshalb für ein gutes Gleichgewicht zwischen Performance und Aktualität.
stale-while-revalidate für eine schnelle Auslieferung
Mit stale-while-revalidate kann auch dann noch eine ältere, aber bereits vorhandene Version ausgeliefert werden, wenn die eigentliche Cache-Laufzeit abgelaufen ist. Parallel dazu wird im Hintergrund bereits eine neue Version aufgebaut. Für Benutzer wirkt die Seite dadurch weiterhin schnell.
Eigene Header für das Prüfen des Cache-Verhaltens
Ein Header wie X-Cache-Status: HIT oder MISS ist beim Testen sehr hilfreich. Gerade bei komplexeren Caching-Strategien lässt sich damit schnell nachvollziehen, ob die gewünschte Seite wirklich aus dem Cache kommt oder noch regulär gerendert wird.
Unsere wichtigste Erkentnis
Für uns war die wichtigste Erkenntnis, dass Edge-Caching auf Cloudflare Workers am besten funktioniert, wenn die Zuständigkeiten klar getrennt sind: erst prüfen, dann speichern. In Kombination mit inhaltsabhängigen Laufzeiten und einer gezielten Invalidierung bei Veröffentlichungen in Storyblok entsteht so eine Lösung, die sowohl performant als auch wartbar ist.
Gerade bei einer größeren Nuxt-Anwendung mit vielen Websites und selten geänderten Inhalten hat sich dieser Ansatz für uns deutlich bewährt. Seiten werden schneller ausgeliefert, die Last auf Rendering und CMS sinkt, und trotzdem bleiben veröffentlichte Änderungen schnell sichtbar.