Tausend Schreibvorgänge pro Tag scheinen auszureichen, bis Sie mit echten Benutzern etwas in die Produktion bringen. Ein Anmeldesystem, das zur Authentifizierung ein Sitzungstoken in den KV schreibt, erschöpft dieses Limit bei einer halben Stunde mäßigem Datenverkehr. Die erste Konfrontation mit den realen Grenzen des KV kommt in der Inszenierung selten vor.
Das kostenlose Kontingent – 100.000 Lese- und 1.000 Schreibvorgänge pro Tag – wurde so konzipiert, dass es das richtige Nutzungsmodell widerspiegelt: viel Lesen, minimales Schreiben. Wenn ein Team KV konsequent mit diesem Modell nutzt, bleibt die kostenlose Stufe lange bestehen. Bei Verwendung als Sitzungsbank oder Zustandsspeicher pro Benutzer erscheint das Limit in der ersten Woche.
Muster, die funktionieren
Die solideste Verwendung von KV ist die Speicherung der Anwendungskonfiguration, die wiederholt gelesen und selten geändert wird. Ein JSON mit Produktflags, Geschäftsparametern, Whitelists und Endpunkten von Drittanbietern – diese Art von Daten ändert sich pro Verwaltungsaktion, nicht pro Benutzer. Ein Schreibvorgang an den KV wird an alle PoPs weitergegeben und bearbeitet Millionen von Anfragen ohne nennenswerte Kosten. Ein Team, das zehnmal im Monat auf diesen Schlüssel schreibt und ihn zehn Millionen Mal liest, liegt problemlos im kostenlosen Kontingent.
Das Zwischenspeichern von gerendertem HTML folgt dem gleichen Prinzip. Ein Blog-Beitrag, eine Produktseite, ein Suchergebnis, das sich nicht pro Benutzer ändert – Sie rendern es einmal, speichern es im KV mit einer entsprechenden TTL und stellen es direkt aus dem PoP-Cache für alle nachfolgenden Anfragen bereit. Die Kosten für das Rendern sinken, die Latenz sinkt und die Anzahl der Schreibvorgänge wird proportional zur Häufigkeit von Inhaltsaktualisierungen, nicht zum Datenverkehrsvolumen.
Es passen auch Sitzungstoken mit TTL, sofern die Sitzung einmalig beschreibbar ist. Sie schreiben das Token bei der Authentifizierung – ein Schreibvorgang pro Anmeldung – und lesen es bei jeder weiteren Anfrage. Wenn sich ein Benutzer einmal anmeldet und stundenlang aktiv ist, ist das Lese-/Schreibverhältnis ausgezeichnet. Was dieses Modell durchbricht, ist die Sitzung mit veränderlichem Zustand: Jede Aktualisierung der Sitzungsdaten wird zu einem Schreibvorgang, und die Kosten explodieren.
Der Trick mit Metadaten
Jeder Schlüssel im KV kann bis zu 1024 Bytes beliebiger JSON-Metadaten enthalten, getrennt vom Wert selbst. Dieses Feld wird von getWithMetadata() zusammen mit dem Wert in einem einzigen Vorgang zurückgegeben – ohne zusätzlichen Leseaufwand.
Der praktische Nutzen besteht darin, Informationen neben dem Wert zu speichern, den Sie auf andere Weise analysieren oder ableiten müssten. Für eine in KV gespeicherte Binärdatei können die Metadaten Content-Type, ETag, Erstellungsdatum, Originalgröße und alle relevanten HTTP-Header enthalten. Der Worker liest den Schlüssel, empfängt Werte und Metadaten in einem Aufruf und stellt die HTTP-Antwort mit den richtigen Headern zusammen, ohne dass zusätzliche Such- oder Analyselogik erforderlich ist.
Dies funktioniert auch für die Light-Versionierung. Speichern Sie die Version oder den Zeitstempel der letzten Aktualisierung in den Metadaten. Jeder Verbraucher kann prüfen, ob er die erwartete Version liest, ohne nach weiteren Daten suchen zu müssen.
Das Leistungsproblem von list()
list() ist im Hinblick auf die relative Leistung die teuerste KV-Operation und erscheint am häufigsten auf Pfaden, die sie nicht verwenden sollten. Ein Aufruf von list() in einem Namespace mit 100.000 Schlüsseln ist langsam – die Latenz hängt von der Größe des Namespace und des Cursors ab – und zählt als Listenvorgang, für den ein separates Kontingent gilt: 1.000 Vorgänge pro Tag im kostenlosen Kontingent, 0,50 $ pro Million im kostenpflichtigen Kontingent.
Das eigentliche Problem besteht darin, list() im Hot Path einer Anfrage zu verwenden. Wenn bei jeder Anfrage ermittelt werden muss, welche Schlüssel zur Bereitstellung einer Antwort vorhanden sind, haben Sie einen Datenverwaltungsvorgang in den kritischen Leistungspfad eingeordnet.
Die Lösung besteht darin, einen Indexschlüssel zu pflegen. Sie schreiben einen Schlüssel wie __index__ in den KV, dessen Wert ein JSON mit der Liste der Namespace-Schlüssel ist – oder nur die für die Logik benötigten Bezeichner. Wenn sich der Namespace ändert, aktualisieren Sie den Index zusammen mit dem Hauptschreibvorgang. Die Kosten betragen einen zusätzlichen Schreibvorgang pro Schreibvorgang. Der Vorteil besteht darin, dass es sich bei jedem Indexlesevorgang um einen regulären Lesevorgang mit Cache-Latenz und ohne die Skalierungsprobleme von list() handelt.
Dieses Muster hat die offensichtliche Einschränkung, dass der Index manuell synchron gehalten werden muss. Wenn Sie über mehrere Writer verfügen, führt das Fehlen atomarer Operationen in KV zu einer Inkonsistenz im Index. Für Namespaces mit Einzelschrift oder einer Schreibschrift, die von einem einzelnen Writer gesteuert wird, funktioniert das Muster gut.
Caching auf Modulebene: die Optimierung, die niemand explizit dokumentiert
Worker auf Cloudflare laufen auf V8-Isolaten. Ein einzelnes Isolat kann Tausende von Anfragen bearbeiten, bevor es vertrieben wird. Im Modulbereich – außerhalb des Handlers – deklarierte Variablen bleiben zwischen Anforderungen bestehen, solange „Isolate“ aktiv ist.
Dadurch entsteht eine einfache und effektive Optimierungsmöglichkeit für Konfigurationsdaten. Anstatt env.CONFIG.get('settings') in jeder Anfrage auszuführen, deklarieren Sie eine Variable im Modulbereich und durchsuchen den KV nur, wenn er noch nicht initialisiert wurde:
let config = null; export default { async fetch(request, env) { config = config ?? await env.CONFIG.get('settings', { type: 'json' }); // usa config } };
Die erste Isolate-Anfrage liest den KV. Alle nachfolgenden Anforderungen für dasselbe Isolat verwenden den In-Memory-Wert. Bei Daten, die sich selten ändern – Produktkonfiguration, Feature-Flags – entfällt dadurch das KV-Lesen fast aller Anfragen, wodurch die Latenz und der Verbrauch von Lesevorgängen reduziert werden.
Dies hat zur Folge, dass sich eine Aktualisierung des KV nicht sofort auf alle Worker auswirkt – jedes Isolat verwendet weiterhin den zwischengespeicherten Wert, bis es entfernt wird. Für Daten, bei denen eine Verzögerung von 60 Sekunden bis zu einigen Minuten akzeptabel ist, ist der Kompromiss ausgezeichnet. Für Daten, die sofort über alle Worker hinweg aktualisiert werden müssen, ist dieses Muster nicht geeignet.
Was man nicht ohne Umdenken in die Produktion übernehmen sollte
Die Verwendung von KV als Jobwarteschlange funktioniert nicht. Ohne atomare Operationen können zwei Worker denselben Job lesen, ihn doppelt verarbeiten und ihn unabhängig voneinander als abgeschlossen markieren. Das Ergebnis ist eine doppelte Verarbeitung ohne Erkennungsmechanismus.
Das Speichern veränderlicher Benutzerdaten per Benutzerschlüssel lässt sich nur schlecht mit der Schreibbeschränkung skalieren. Eine Anwendung mit 10.000 aktiven Benutzern pro Tag, die Profildaten auch nur einmal pro Sitzung aktualisiert, liegt bereits in der Größenordnung des bezahlten Limits von 1 Million monatlichen Schreibvorgängen – und die Kosten pro Schreibvorgang beginnen bei 0,50 $/Million zu erscheinen, wenn man darüber hinausgeht.
Namespaces mit hoher Schlüsseldichte und häufigem Auflistungsbedarf sind eine Leistungsfalle. list() ist in großen Namespaces langsam und sollte nicht im Anforderungspfad sein. Wenn Ihr Anwendungsfall eine häufige Auflistung erfordert, muss das Datenmodell geändert werden – entweder mit manuell gepflegten Indexschlüsseln oder mit einem anderen Tool.
Was die kostenlose Stufe über das Design verrät
Die Grenzwerte des kostenlosen Kontingents – 100.000 Lesevorgänge für 1.000 Schreibvorgänge – sind ein getarntes Designdokument. Das Verhältnis von 100:1 zwischen Lese- und Schreibvorgängen ist nicht willkürlich. Es beschreibt die Arbeitsbelastung, für die der KV konzipiert wurde. Jede Verwendung, die dieses Verhältnis umkehrt oder annähert, liegt außerhalb des beabsichtigten Betriebsmodells und stößt auf Kosten-, Leistungs- oder Konsistenzbeschränkungen, die bei Tests mit geringem Volumen nicht auftreten.
Lesen Sie auch
- Cloudflare KV: Was bedeutet global verteilt, wenn Sie schreiben müssen
- Langlebige Objekte in der Produktion: Wie die Rechnung aussehen wird und welche Grenzen überraschen
- Cache-Invalidierung in KV: das Problem, das niemand elegant löst
- KV für Ratenbegrenzung, Feature-Flags und verteilte Konfiguration: wo es funktioniert und wo es kaputt geht
- D1 in der Produktion: Leistung, Grenzen und was sich nicht alleine skalieren lässt
- KV vs. R2 vs. Cache-API: Wann die einzelnen Cloudflare-Speicherebenen verwendet werden sollten
