Zurück zum Blog

Kosten für OpenNext KV-Speicher auf Cloudflare Workers senken

2026-05-115 min read

Learn English Sounds begann, 0,50 $ pro Tag für Cloudflare KV-Speicher auszugeben. Das klingt nicht nach viel, bis man bedenkt, dass es sich um eine Website mit statischen Inhalten handelt, deren Hosting zuvor fast nichts gekostet hat. Ich habe mich damit beschäftigt und einen klassischen OpenNext-Fallstrick auf Cloudflare gefunden. Wenn Sie Next.js über den @opennextjs/cloudflare-Adapter auf Workers ausführen, betrifft dies wahrscheinlich auch Sie.

Was ist Cloudflare KV?

Cloudflare KV (kurz für Key-Value) ist ein global verteilter Key-Value-Speicher, der für Lese-intensive Workloads entwickelt wurde. Sie schreiben einen Wert einmal und er wird schließlich weltweit an Rechenzentren repliziert, sodass Lesevorgänge von überall schnell sind. Die Preisgestaltung besteht aus zwei Komponenten: Operationen (Lesen, Schreiben, Löschen) und Speicher (pro GB-Monat berechnet). Die ersten 1 GB Speicher sind enthalten, dann kostet jeder weitere GB-Monat etwa 0,50 $. Diese zweite Zahl ist es, die Sie erwischt, wenn etwas den Namespace leise mit veralteten Daten füllt.

Für OpenNext ist KV der Ort, an dem gerenderte Seiten zwischengespeichert werden, damit zukünftige Anfragen nicht erneut gerendert werden müssen. Es ist schnell, kostengünstig pro Operation und ideal zum Caching von HTML und JSON. Die Falle ist, dass nichts in KV automatisch abläuft. Wenn Sie einen Schlüssel schreiben, bleibt er bestehen, bis Sie ihn löschen.

Die Einrichtung

Die Website verwendet OpenNext, um Next.js auf Cloudflare Workers bereitzustellen. Der inkrementelle Cache (die Speicherschicht hinter ISR und Full-Route-Caching) ist so konfiguriert, dass KV mit einem regionalen Cache-Wrapper verwendet wird. Die relevante Konfiguration sieht wie folgt aus:

import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import kvIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache";
import { withRegionalCache } from "@opennextjs/cloudflare/overrides/incremental-cache/regional-cache";
import d1TagCache from "@opennextjs/cloudflare/overrides/tag-cache/d1-next-tag-cache";

export default defineCloudflareConfig({
  incrementalCache: withRegionalCache(kvIncrementalCache, {
    mode: "long-lived",
    bypassTagCacheOnCacheHit: true,
  }),
  tagCache: d1TagCache,
});

Dies ist genau das, was die OpenNext-Dokumentation empfiehlt. Statische Seiten werden zur Build-Zeit gerendert, in KV geschrieben und auf dem Weg nach außen von einer regionalen Cache-API-Schicht bedient. Die CPU-Auslastung sinkt, da der Worker selten etwas rendern muss. Die Speicherkosten sollten ein Rundungsfehler sein.

Was ist der regionale Cache?

Es lohnt sich, hier kurz innezuhalten, denn es erklärt, warum das Bereinigen von KV die Leistung nicht beeinträchtigt. Cloudflare gibt jedem Worker Zugriff auf die Cache-API, einen regionsspezifischen In-Memory-Cache, der sich im Edge-Rechenzentrum befindet, das der Anfrage getroffen hat. OpenNexts withRegionalCache umschließt den KV-inkrementellen Cache und verwendet diese Cache-API als erste Schicht.

Die Abruf-Reihenfolge für jede zwischengespeicherte Seite ist: zuerst die regionale Cache-API, dann KV, dann erneutes Rendern. Die erste Anfrage in einer Region holt die Seite aus KV und speichert sie im lokalen Cache. Jede nachfolgende Anfrage in dieser Region wird aus dem Speicher am Edge bedient, ohne KV zu berühren oder den Render-Code des Workers auszuführen. Mit mode: "long-lived" bleiben die regionalen Kopien so lange erhalten, wie Cloudflare sie behält, was auf einem warmen Knoten typischerweise Minuten bis Stunden dauert.

KV ist also Cold Storage. Es wird nur gelesen, wenn der Cache einer Region kalt oder geleert ist. Das Bereinigen nicht referenzierter KV-Schlüssel ändert nicht, wie oft KV gelesen wird; es ändert nur, wie viel Sie speichern. Die CPU bewegt sich nicht, da sich nichts am Hot Path geändert hat.

Was ich tatsächlich in KV gefunden habe

Ich habe die Schlüssel im Namespace aufgelistet. Es gab 276.637 davon. Jeder einzelne begann mit incremental-cache/. Die Gruppierung nach dem zweiten Pfadsegment erzählte die wahre Geschichte:

$ wrangler kv key list --namespace-id <id> --remote \
    | jq -r '.[].name' | awk -F/ '{print $2}' \
    | sort | uniq -c | sort -rn | head

17896 Mqxc5oa6LB-nPqyGFrVbV
13068 zMp2Qi0YY-gVHmaXRB9Ce
 9108 zMlI9hb_vFtk705L8GDKL
 8748 -KcEM0U2hAvoQH7n5dyV5
  ...
 5895 W-RYs65XPM1m0Dg899-cz

Neununddreißig verschiedene Präfixe. Jeder davon ist eine Next.js BUILD_ID aus einem früheren Deploy. Jedes next build generiert eine neue Build-ID. Jedes Deploy schreibt ein neues Universum von Cache-Einträgen, die auf diese ID beschränkt sind. Der Live-Worker liest immer nur das Präfix seines eigenen Builds. Alte Präfixe sind reines totes Gewicht, und nichts in OpenNext oder Cloudflare bereinigt sie für Sie.

So sammelt eine aktive Website leise über 250.000 verwaiste Schlüssel an.

Warum Ihr aktiver Build auch wächst

Es gibt ein zweites Muster, das in dieser Liste erwähnenswert ist. Das größte Präfix hatte 17.896 Schlüssel, das nächstgrößte 13.068, und die meisten anderen lagen um 6.500. Das große ist der aktuell aktive Build. Er hat mehr Schlüssel als die anderen, weil der tatsächliche Traffic im Laufe der Zeit immer neue Long-Tail-Seiten hinzufügt. Das ist normal und in Ordnung. Das Problem sind all die darunter.

Die Lösung

Der aktive Worker liest nur aus incremental-cache/<current-BUILD_ID>/. Löschen Sie also nach jedem erfolgreichen Deploy jeden Schlüssel, der nicht mit diesem Präfix beginnt. Die Schlüssel, die Sie löschen, sind garantiert nicht von einem Live-Code-Pfad referenziert. Dies ist keine Cache-Invalidierung, die CPU kostet; es ist das Aufkehren von Müll, der nie gelesen wird.

Ein Skript zum Bereinigen, das nach dem Deploy ausgeführt wird

Ich habe ein Node-Skript zum Projekt hinzugefügt und es in den Deploy-Befehl integriert. Es liest die Build-ID aus der OpenNext-Build-Ausgabe, listet alle KV-Schlüssel auf und löscht massenhaft alles außerhalb des aktiven Präfixes in 10.000er-Chunks (das Limit für Wrangler-Massenlöschungen).

// scripts/prune-kv-cache.mjs
// ---------------------------------------------------------------
// Löscht veraltete OpenNext inkrementelle Cache-Einträge aus Cloudflare KV
// nach jedem erfolgreichen Deploy. Wird als Post-Deploy-Schritt ausgeführt.
// ---------------------------------------------------------------

import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { tmpdir } from "node:os";
import { join } from "node:path";

// Cloudflare KV-Namespace, der den inkrementellen Cache von OpenNext sichert.
// Finden Sie diese ID in wrangler.toml unter [[kv_namespaces]] für NEXT_INC_CACHE_KV.
const KV_NAMESPACE_ID = "<your-namespace-id>";

// OpenNext schreibt Cache-Schlüssel unter `incremental-cache/<BUILD_ID>/...`.
// Der aktive Worker liest nur seine eigene BUILD_ID, daher ist jedes andere Präfix Müll.
// Wir lesen die frisch erstellte BUILD_ID von der Festplatte, damit das Skript immer
// mit dem gerade erfolgten Deploy übereinstimmt.
const KEEP_BUILD_ID = readFileSync(".open-next/assets/BUILD_ID", "utf8").trim();

// Wranglers `kv bulk delete` akzeptiert maximal 10.000 Schlüssel pro Aufruf.
const CHUNK_SIZE = 10000;

// 1) Liste jeden Schlüssel im Namespace auf.
// maxBuffer wird auf 512 MB erhöht, da große Websites zehntausende MB JSON zurückgeben können.
const list = spawnSync(
  "npx",
  ["wrangler", "kv", "key", "list", "--namespace-id", KV_NAMESPACE_ID, "--remote"],
  { encoding: "utf8", maxBuffer: 1024 * 1024 * 512 }
);

// 2) Entscheiden Sie, was behalten und was gelöscht werden soll.
// Behalten:   incremental-cache/<KEEP_BUILD_ID>/...   (der Live-Worker liest von hier)
// Löschen: incremental-cache/<alles andere>/...  (verwaist von früheren Deploys)
// Nicht-Cache-Schlüssel werden durch die zweite `startsWith`-Prüfung ignoriert.
const keepPrefix = `incremental-cache/${KEEP_BUILD_ID}/`;
const toDelete = JSON.parse(list.stdout)
  .map((k) => k.name)
  .filter((n) => n.startsWith("incremental-cache/") && !n.startsWith(keepPrefix));

// 3) Löschen Sie die verwaisten Schlüssel in 10k-Schlüssel-Chunks massenhaft.
// Wrangler liest jeden Chunk aus einer JSON-Datei auf der Festplatte, daher verwenden wir ein temporäres Verzeichnis
// und räumen es am Ende auf, unabhängig von Erfolg oder Misserfolg.
const workDir = mkdtempSync(join(tmpdir(), "kv-prune-"));
for (let i = 0; i < toDelete.length; i += CHUNK_SIZE) {
  const chunkPath = join(workDir, `chunk_${i}.json`);
  // Wrangler erwartet ein JSON-Array von Schlüsselnamen, z. B. ["key1", "key2", ...]
  writeFileSync(chunkPath, JSON.stringify(toDelete.slice(i, i + CHUNK_SIZE)));
  spawnSync(
    "npx",
    ["wrangler", "kv", "bulk", "delete", "--namespace-id", KV_NAMESPACE_ID, "--remote", chunkPath],
    { stdio: "inherit" } // Streamen Sie Wranglers Fortschrittsbalken direkt ins Terminal
  );
}

// 4) Räumen Sie das temporäre Verzeichnis auf und berichten Sie, was wir getan haben.
rmSync(workDir, { recursive: true, force: true });
console.log(`Pruned ${toDelete.length} stale cache keys.`);

Dann habe ich es in die Deploy-Kette in package.json eingebunden, damit es jedes Mal automatisch ausgeführt wird:

"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy --cacheChunkSize 1000 && npm run prune-kv-cache",
"prune-kv-cache": "node scripts/prune-kv-cache.mjs"

Die Reihenfolge ist wichtig. Zuerst bauen, damit die neue BUILD_ID auf der Festplatte existiert. Dann deployen, damit der neue Worker live ist und aus dem neuen Präfix liest. Dann bereinigen, damit das alte Präfix sicher gelöscht werden kann, sobald der Übergang abgeschlossen ist.

Das Ergebnis

Auf der bereinigten Website ging KV von 276.637 Schlüsseln über 39 Builds auf 7.619 Schlüssel im einzigen aktiven Build zurück, eine Reduzierung von etwa 97 %. Die prognostizierten täglichen Speicherkosten sanken von 0,50 $ auf wenige Cent. Keine Code-Pfad-Änderungen, keine Änderungen der Caching-Strategie, keine CPU-Regression. Der regionale Cache umschließt KV weiterhin genau wie zuvor, sodass Hot Reads aus der Cache-API kommen und nie den Speicher berühren.

Wann Sie nicht bereinigen sollten

Die Bereinigung ist die richtige Standardeinstellung für Single-Version-Deploys, aber es gibt reale Situationen, in denen sie Ihnen schaden kann.

Sofortiges Rollback als Sicherheitsnetz. Wenn Sie sich auf wrangler rollback verlassen, um in Sekundenschnelle zu einer früheren Worker-Version zurückzukehren, ist der Cache der vorherigen Version nach einer Bereinigung weg. Das Rollback funktioniert weiterhin, aber die erste Welle von Anfragen nach dem Rollback wird alle fehlschlagen und neu gerendert. Die CPU steigt für ein paar Minuten an. Ändern Sie das Skript, um die letzten N Build-IDs anstelle von nur der aktiven zu behalten, wenn dies wichtig ist.

Rolling Releases oder Canary Deploys. Wenn Cloudflare Rolling Releases oder eine Art schrittweiser Rollout im Spiel sind, bedienen mehrere Worker-Versionen gleichzeitig den Traffic. Jede benötigt ihr eigenes Build-Präfix intakt. Bereinigen Sie nicht, bis der Rollout 100 % erreicht hat.

Laufende Anfragen während des Deploys. Es gibt immer ein kurzes Zeitfenster, in dem der alte Worker kurz vor dem Start des neuen noch ein paar Nachzügler bedienen kann. Diese werden den Cache verfehlen und einmal neu gerendert. Für die meisten Websites ist dies unsichtbar. Es ist gut zu wissen, wenn Sie burstigen Traffic handhaben.

Ein Hinweis zu R2 als Alternative

OpenNext liefert auch einen R2-basierten inkrementellen Cache. R2-Speicher ist etwa 33-mal günstiger als KV (etwa 0,015 $ gegenüber 0,50 $ pro GB-Monat zum Zeitpunkt des Schreibens), und der regionale Cache-Wrapper sitzt davor, genauso wie er vor KV sitzt. Ihre Hot-Path-Leistung ändert sich also nicht, aber der Cold Storage wird dramatisch günstiger. Die Bereinigung ist aus hygienischen Gründen immer noch sinnvoll, aber der Kostendruck verschwindet größtenteils. Wenn Ihre Website groß genug ist, dass selbst der Cache des aktiven Builds beträchtlich ist, ist ein Wechsel zu R2 einen Blick wert.

Auf dem Laufenden bleiben

Erhalten Sie die neuesten Beiträge und Einblicke direkt in Ihren Posteingang.

Unsubscribe anytime. No spam, ever.