Cloudflare Workers KV in der Praxis: Verteilter Key-Value-Speicher von den Grundlagen bis zur Profi-Nutzung
Sie starren auf die Latenzkurve im Cloudflare Dashboard. Die rote Linie hängt noch über 200 ms – obwohl Sie Workers nutzen und der Code schlank ist. Warum wartet jede Nutzeranfrage trotzdem so lange?
Das Problem liegt oft in der Datenbank. Jede Session-Abfrage läuft vom Edge-Knoten ins Rechenzentrum in Europa und zurück. Selbst wenn der Worker nur 5 ms braucht, frisst die Netzwerkstrecke den Rest.
Workers sind stateless. Sie brauchen Speicher, der wirklich am Edge lebt – Cloudflare Workers KV.
Dieser Artikel bündelt Fehler, Messwerte und Code aus der Praxis: was KV ist, warum sub-10ms möglich sind, vollständige Session- und API-Cache-Implementierungen sowie wann D1 oder R2 die bessere Wahl sind.
Was ist KV – verteilter Edge-Speicher verstehen
Kurz gesagt: KV ist der „mitlaufende Speicher“ von Cloudflare für Workers. Er sitzt nicht in einem einzelnen Rechenzentrum, sondern verteilt sich über 300+ Edge-Knoten weltweit. Eine Anfrage aus Tokio trifft Daten oft direkt in Tokio; aus Frankfurt oft schon im Frankfurter Cache.
Cloudflare Workers KV ist ein global verteilter Key-Value-Speicher für Edge Computing. Drei Merkmale prägen ihn:
Extrem schnelle Reads. Hot Keys liegen bei Cache-Treffern zwischen 500µs und 10 ms – beim ersten Blick skeptisch, bis eigene Benchmarks das bestätigen: stabil im einstelligen Millisekundenbereich.
Globale Replikation. Ein Write wird an alle Edge-Knoten repliziert. Anders als bei vielen Redis-Clustern gilt: einmal schreiben, überall lesen – ideal für read-heavy Workloads.
Hoher Read-Durchsatz. Einzelne Keys können tausende RPS lesen, weil Daten am Edge gecacht werden und nicht jedes Mal die Zentralinstanz anfahren müssen.
Cloudflare-Speicher im Überblick
KV ist ein Teil der Cloudflare-Speichermatrix:
| Speicherdienst | Datenmodell | Beste Szenarien | Schreiblimit | Latenz |
|---|---|---|---|---|
| KV | Key-Value | Session, Cache, Konfiguration | 1 RPS/key | Hot Keys 500µs–10ms |
| D1 | SQL (SQLite) | Nutzerdaten, Bestellungen, Reports | kein hartes Limit | oft 50–200ms, standortabhängig |
| R2 | Object Storage | Dateien, Bilder, Video | kein hartes Limit | Download schnell, Upload abhängig von Größe |
| Durable Objects | Stateful Objects | Kollaboration, WebSocket | kein hartes Limit | Bindung an bestimmten Knoten |
Was bedeutet 1 RPS/key? Pro Key nur ein Write pro Sekunde – das wichtigste Limit von KV, später im Detail.
Wann KV – Kurzentscheidung
KV passt gut:
- Session Storage (Login-Status)
- API-Response-Cache (Drittanbieter-APIs)
- Rate-Limiting-Zähler
- Feature Flags / Konfiguration
- Redirect-Mapping (URL-Regeln)
KV eher nicht:
- häufige Writes am selben Key (über 1 RPS)
- komplexe SQL-Daten (Nutzer-, Bestelltabellen → D1)
- große Dateien (Bilder, Video → R2)
- starke Konsistenz (Finanztransaktionen → Durable Objects)
In der offiziellen Doku: KV für „hohe Leserate, seltene Änderungen, keine sofortige Konsistenz“. Frameworks wie OpenAuth nutzen KV standardmäßig für Sessions – unten folgt vollständiger Beispielcode.
KV-Architektur – warum es so schnell ist
Die Geschwindigkeit kommt von drei Cache-Ebenen, nicht von Magie.
Stellen Sie sich einen Kiosk vor: Ware direkt an der Theke (Edge Cache); sonst aus dem Lager hinten (Regional); im Notfall aus dem Zentrallager per LKW (Central Store).
Anfrage → Edge Cache (schnellste)
↓ Miss
Regional Cache
↓ Miss
Central Store (langsamste)
Laut Cloudflare-Blog (Oktober 2025) lösen etwa 30 % der Requests alles in der Cache-Schicht – ein Drittel der Reads ohne Zentralzugriff, daher die niedrige Latenz.
Performance: Doku und eigene Messung
Referenzwerte aus der Doku:
- Hot keys: 500µs bis 10 ms
- Cold keys: höhere Latenz, Backhaul nötig
Ein einfacher Latenztest:
// Einfacher Latenztest
const start = Date.now();
await env.KV.get("test-key");
const latency = Date.now() - start;
console.log(`Latency: ${latency}ms`);
100 Durchläufe: Hot Keys im Schnitt 5–8 ms. Cold Keys beim ersten Zugriff 50 ms+, danach deutlich schneller – Cache greift.
2025 beschleunigte Cloudflare KV laut Blog etwa 3×, u. a. durch:
- direkte Verbindung Workers ↔ KV ohne frühere Front-Line-Schicht
- kürzere interne Datenpfade
Auch abhängige Dienste (Turnstile, Waiting Room) profitieren davon.
Konsistenz: Preis der Eventual Consistency
KV ist eventually consistent: ein Write erscheint nicht sofort auf jedem Edge-Knoten. In Tests: regionale Propagation oft Sekunden bis wenige Dutzend Sekunden.
Problematisch:
- Session gerade geschrieben, nächste Anfrage auf anderem Edge – Session „fehlt“
- Echtzeit-Kollaboration: A schreibt, B liest sofort – veralteter Stand
Meist unkritisch:
- Feature Flags – Sekunden Verzögerung OK
- API-Cache – Minuten TTL, Propagation egal
- Redirect-Regeln – Nutzer merken kaum etwas
Brauchen Sie sofortige Konsistenz, sind Durable Objects oft besser – fester Knoten, garantierter Zustand.
Wrangler CLI – Setup in der Praxis
Zwei Schritte: Namespace anlegen, in wrangler.toml an den Worker binden.
Namespace anlegen
Ein Namespace ist der „Container“ für beliebig viele Key-Value-Paare. Pro Konto bis 1000 Namespaces (Anfang 2025 von 200 erhöht).
# Production-Namespace
wrangler kv namespace create MY_KV
# Ausgabe ähnlich:
# Created namespace with id "abc123def456..."
# Add the following to your wrangler.toml:
# [[kv_namespaces]]
# binding = "MY_KV"
# id = "abc123def456..."
Für lokale Entwicklung zusätzlich ein Preview-Namespace:
# Preview-Namespace
wrangler kv namespace create MY_KV --preview
# Ausgabe ähnlich:
# Created preview namespace with id "preview_abc123..."
wrangler.toml
IDs aus der CLI in die Konfiguration:
name = "my-worker"
main = "src/index.ts"
[[kv_namespaces]]
binding = "MY_KV"
id = "abc123def456..." # Production
preview_id = "preview_abc123..." # Preview (lokal)
binding legt den Namen in Code fest:
// binding = "MY_KV" → env.MY_KV
const value = await env.MY_KV.get("some-key");
REST API vs. Workers Binding API
Workers Binding API (empfohlen):
- direkt
env.MY_KV.get()im Worker - kein extra HTTP-Hop, schnellste Variante
- kostenlos (nur Worker-Laufzeit)
REST API:
- HTTP-Zugriff auf KV
- Token-Auth, für externe Systeme
- unterliegt den globalen REST-Rate-Limits
Fast immer Binding API. REST eher für externe Tools, CI/CD-Bulk-Import oder Ops-Debugging.
Häufige Wrangler-KV-Befehle
# Schreiben
wrangler kv key put --namespace-id=abc123 "my-key" "my-value"
# Lesen
wrangler kv key get --namespace-id=abc123 "my-key"
# Löschen
wrangler kv key delete --namespace-id=abc123 "my-key"
# Keys auflisten (mit Präfix)
wrangler kv key list --namespace-id=abc123 --prefix="session:"
Praktisch zum Debuggen; in Produktion lieber Worker-Code.
TypeScript – Praxiscode
Vollständige, lauffähige Beispiele.
Basis-CRUD
// src/index.ts
interface Env {
MY_KV: KVNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
// Schreiben
if (path === "/put") {
const key = url.searchParams.get("key") || "default";
const value = url.searchParams.get("value") || "hello";
await env.MY_KV.put(key, value);
return new Response(`Saved: ${key} = ${value}`);
}
// Lesen
if (path === "/get") {
const key = url.searchParams.get("key") || "default";
const value = await env.MY_KV.get(key);
if (value === null) {
return new Response("Key not found", { status: 404 });
}
return new Response(value);
}
// Löschen
if (path === "/delete") {
const key = url.searchParams.get("key") || "default";
await env.MY_KV.delete(key);
return new Response(`Deleted: ${key}`);
}
// Keys listen (mit Präfix)
if (path === "/list") {
const prefix = url.searchParams.get("prefix") || "";
const keys = await env.MY_KV.list({ prefix });
const keyList = keys.keys.map(k => k.name).join("\n");
return new Response(keyList || "No keys found");
}
return new Response("Try /put, /get, /delete, or /list");
},
};
Lokal testen:
wrangler dev
# Schreiben
curl "http://localhost:8787/put?key=test&value=helloworld"
# Lesen
curl "http://localhost:8787/get?key=test"
Session Storage – vollständig
// src/session.ts
interface SessionData {
userId: string;
email: string;
createdAt: number;
expiresAt: number;
}
interface Env {
SESSION_KV: KVNamespace;
}
const SESSION_TTL = 3600; // 1 Stunde
class SessionManager {
private kv: KVNamespace;
constructor(kv: KVNamespace) {
this.kv = kv;
}
async create(userId: string, email: string): Promise<string> {
const sessionId = crypto.randomUUID();
const sessionData: SessionData = {
userId,
email,
createdAt: Date.now(),
expiresAt: Date.now() + SESSION_TTL * 1000,
};
await this.kv.put(
`session:${sessionId}`,
JSON.stringify(sessionData),
{ expirationTtl: SESSION_TTL }
);
return sessionId;
}
async get(sessionId: string): Promise<SessionData | null> {
const raw = await this.kv.get(`session:${sessionId}`);
if (!raw) return null;
try {
return JSON.parse(raw) as SessionData;
} catch {
return null;
}
}
async delete(sessionId: string): Promise<void> {
await this.kv.delete(`session:${sessionId}`);
}
async refresh(sessionId: string): Promise<boolean> {
const session = await this.get(sessionId);
if (!session) return false;
session.expiresAt = Date.now() + SESSION_TTL * 1000;
await this.kv.put(
`session:${sessionId}`,
JSON.stringify(session),
{ expirationTtl: SESSION_TTL }
);
return true;
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const sessionManager = new SessionManager(env.SESSION_KV);
const url = new URL(request.url);
if (url.pathname === "/login" && request.method === "POST") {
const body = await request.json();
const sessionId = await sessionManager.create(
body.userId as string,
body.email as string
);
return new Response(JSON.stringify({ sessionId }), {
headers: { "Content-Type": "application/json" },
});
}
if (url.pathname === "/verify") {
const sessionId = url.searchParams.get("sessionId");
if (!sessionId) {
return new Response("Missing sessionId", { status: 400 });
}
const session = await sessionManager.get(sessionId);
if (!session) {
return new Response("Session not found", { status: 401 });
}
return new Response(JSON.stringify(session), {
headers: { "Content-Type": "application/json" },
});
}
if (url.pathname === "/logout") {
const sessionId = url.searchParams.get("sessionId");
if (sessionId) {
await sessionManager.delete(sessionId);
}
return new Response("Logged out");
}
return new Response("Not found", { status: 404 });
},
};
Wichtig:
- TTL:
expirationTtllöscht abgelaufene Einträge automatisch - Key-Präfix:
session:trennt Datentypen und erleichtertlist - JSON: KV speichert nur Strings –
JSON.stringify/parsenötig
API-Response-Cache
// src/api-cache.ts
interface Env {
CACHE_KV: KVNamespace;
}
const DEFAULT_CACHE_TTL = 300; // 5 Minuten
async function cachedFetch(
kv: KVNamespace,
cacheKey: string,
url: string,
ttl: number = DEFAULT_CACHE_TTL
): Promise<Response> {
const cached = await kv.get(cacheKey, "text");
if (cached) {
console.log(`Cache hit: ${cacheKey}`);
return new Response(cached, {
headers: {
"Content-Type": "application/json",
"X-Cache": "HIT",
},
});
}
console.log(`Cache miss: ${cacheKey}`);
const response = await fetch(url);
const body = await response.text();
await kv.put(cacheKey, body, {
expirationTtl: ttl,
});
return new Response(body, {
headers: {
"Content-Type": "application/json",
"X-Cache": "MISS",
},
});
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const apiUrl = url.searchParams.get("api");
if (!apiUrl) {
return new Response("Missing api parameter", { status: 400 });
}
const cacheKey = `api:${apiUrl}`;
return cachedFetch(env.CACHE_KV, cacheKey, apiUrl);
},
};
cacheTtl optimieren
Oft übersehen: cacheTtl steuert die Edge-Cache-Dauer (Standard 60 s). Für Hot Data höher setzen:
// Konfiguration mit längerem Edge-Cache
await env.MY_KV.get("config:feature-flags", {
cacheTtl: 3600, // 1 Stunde Edge-Cache
});
Auch wenn sich der Central Store nicht ändert, hält der Edge den Wert länger – ideal für Feature Flags ohne Echtzeit-Zwang.
KV vs. D1 vs. R2 – Entscheidungshilfe
Welcher Datentyp?
├─ Dateien (Bilder, Video, PDF)?
│ └─ JA → R2
│
├─ SQL (Nutzer, Bestellungen, Joins)?
│ └─ JA → D1
│
├─ Einfaches Key-Value, read-heavy?
│ ├─ Writes > 1 RPS/key?
│ │ └─ JA → nicht KV → D1 oder Durable Objects
│ │
│ └─ NEIN → KV ✓
│
├─ Sofortige Konsistenz nötig?
│ └─ JA → Durable Objects
│ └─ NEIN → KV oft OK
│
└─ Unsicher?
└─ erst KV testen – reicht es, nicht wechseln
Vergleichstabelle
| Dimension | KV | D1 | R2 |
|---|---|---|---|
| Modell | Key-Value | SQL (SQLite) | Object Storage |
| Abfragen | get/put/delete | volles SQL | Pfad, kein Query |
| Schreiblimit | 1 RPS pro Key | kein hartes Limit | kein hartes Limit |
| Read-Latenz | 500µs–10ms (hot) | 50–200ms | schnell (Download) |
| Konsistenz | eventual | stark (Single Region) | eventual |
| Max. Value | 25 MB | SQLite-Zeilenlimit | 5 TB pro Objekt |
| Free Tier | 100k reads/Tag | 5 GB + 25M row reads | 10 GB Speicher |
| Typisch | Session, Cache, Config | Nutzer, Orders, Reports | Dateien, Backup |
Szenario-Empfehlungen
Auth / Session → KV
Einfaches Key-Value, viele Reads pro Request, wenige Writes. OpenAuth & Co. nutzen KV standardmäßig.
await env.SESSION_KV.put(`session:${sessionId}`, JSON.stringify(session));
Profile / Bestellungen → D1
SQL nötig – KV kann keine Joins oder Aggregationen.
SELECT * FROM orders WHERE user_id = ? AND created_at > ?
Bilder / Dateien → R2
Größer als 25 MB oder reines Blob-Storage – nicht KV.
await env.MY_BUCKET.put("images/profile.jpg", imageBuffer);
API Rate Limiting → KV (vorsichtig)
Zähler sind Key-Value, Writes können 1 RPS/key sprengen. Grobe Tageslimits OK; präzises per-Second-Limit eher Durable Objects oder Upstash Redis.
const count = parseInt(await env.KV.get(`rate:${userId}`) || "0");
if (count > 100) {
return new Response("Rate limit exceeded", { status: 429 });
}
await env.KV.put(`rate:${userId}`, String(count + 1));
Drittanbieter-API-Cache → KV
Viele Reads, seltene Writes, Minuten-TTL – Eventual Consistency reicht.
Kombination im Projekt
interface Env {
SESSION_KV: KVNamespace;
CACHE_KV: KVNamespace;
DATABASE_D1: D1Database;
FILES_R2: R2Bucket;
}
// Ein Request kann alles nutzen:
// 1. SESSION_KV für Session
// 2. CACHE_KV für API-Cache
// 3. DATABASE_D1 für Bestellungen
// 4. FILES_R2 für Avatar
So entfaltet sich die Cloudflare-Speichermatrix im Alltag.
Performance-Optimierung
1. cacheTtl tunen
// ❌ Standard: 60 s Edge-Cache
await env.KV.get("config:feature-flags");
// ✅ Konfiguration: länger cachen
await env.KV.get("config:feature-flags", {
cacheTtl: 3600,
});
Erhöhen: Feature Flags, statische Endpoints, Redirect-Tabellen.
Nicht erhöhen: Live-Sessions, Echtzeit-Zähler.
2. Parallel statt seriell
// ❌ Seriell: Latenz addiert sich
const user = await env.KV.get(`user:${userId}`);
const settings = await env.KV.get(`settings:${userId}`);
const permissions = await env.KV.get(`permissions:${userId}`);
// ✅ Parallel: Latenz ≈ Maximum eines Reads
const [user, settings, permissions] = await Promise.all([
env.KV.get(`user:${userId}`),
env.KV.get(`settings:${userId}`),
env.KV.get(`permissions:${userId}`),
]);
Messung: 3 Keys seriell ~20 ms, parallel ~8 ms.
3. Hot-Key-Design
// ❌ Nur ein Nutzer liest → cold
await env.KV.get(`session:${userId}`);
// ✅ Gemeinsame Daten → hot
await env.KV.get("config:global-flags");
Nicht alles in einen Key packen: private Daten pro User-ID, globale Config als ein Hot Key.
4. Namespace-Organisation
[[kv_namespaces]]
binding = "SESSION_KV"
id = "xxx"
[[kv_namespaces]]
binding = "CACHE_KV"
id = "yyy"
[[kv_namespaces]]
binding = "CONFIG_KV"
id = "zzz"
Vorteile: getrenntes Aufräumen, unterschiedliche TTL-Strategien, getrenntes Monitoring im Dashboard.
5. Batch mit list()
const result = await env.SESSION_KV.list({ prefix: "session:" });
for (const key of result.keys) {
console.log(key.name);
}
if (!result.list_complete) {
const next = await env.SESSION_KV.list({
prefix: "session:",
cursor: result.cursor,
});
}
Bulk-Delete vorsichtig – viele Writes verbrauchen Kontingente.
Preise und Limits
Free vs. Paid
| Metrik | Free | Paid ($5/Monat) |
|---|---|---|
| Reads | 100.000 / Tag | unbegrenzt (metered) |
| Writes | 1.000 / Tag | unbegrenzt (metered) |
| Deletes | 1.000 / Tag | unbegrenzt (metered) |
| Lists | 1.000 / Tag | unbegrenzt (metered) |
| Speicher | 1 GB | unbegrenzt (metered) |
| Namespaces | 1000 | 1000 |
Free reicht für Tests und kleine Projekte. Produktion oft Paid: keine Read-Caps, mehr Writes, Dashboard-Alerts.
Write Rate Limit – 1 RPS/key
Pro unique key maximal 1 Write pro Sekunde.
// ❌ Schleife überschreitet Limit
for (let i = 0; i < 10; i++) {
await env.KV.put("counter", String(i));
}
// ✅ Zeitbasierte Keys
await env.KV.put(`counter:${Math.floor(Date.now() / 1000)}`, value);
Hintergrund: globale Replikation pro Write – Cloudflare schützt die Plattform mit 1 RPS/key.
Strategien: Zeit- oder UUID-Keys verteilen; bei echtem High-Frequency-Write D1 oder Durable Objects.
Value-Größe
Maximal 25 MB. Größere Daten → R2.
Namespace-Strategie
Bis 1000 Namespaces pro Konto. Funktionsweise trennen oder per Präfix in einem Namespace:
await env.KV.put("session:user1", data);
await env.KV.put("cache:api1", data);
await env.KV.put("config:flags", data);
Kostenschätzung (Paid)
Monat = $5 (Basis) + Reads + Writes + Speicher
Reads = Anzahl × $0,01 / 100.000
Writes = Anzahl × $1,00 / 1.000.000
Speicher = GB × $0,50
Beispiel 100k Requests/Tag, 30 Tage: oft nur wenige Dollar über dem $5-Basispreis.
Fazit
KV ist der „mitlaufende Speicher“ von Workers – für Session, Cache und Config mit vielen Reads und wenigen Writes.
KV, wenn:
- einfaches Key-Value
- Reads ≫ Writes
- Eventual Consistency OK
- ≤ 1 Write/Sekunde pro Key
D1, wenn SQL, Joins oder > 1 RPS Writes nötig sind.
R2, wenn Dateien oder Values > 25 MB.
Durable Objects, wenn sofortige Konsistenz oder Echtzeit-Kollaboration.
Als Nächstes: Session-Code oben mit wrangler dev ausprobieren. Mehr zur Speichermatrix in der Serie cloudflare-bindui – D1 und R2 dort vertieft.
FAQ
Welche Schreiblimits hat Cloudflare Workers KV?
Eignet sich KV für User-Sessions?
Was ist der Unterschied zwischen KV und D1 – wann was?
• KV: Key-Value-Modell, Hot Keys 500µs–10ms, Schreiblimit 1 RPS/key
• D1: SQL (SQLite), komplexe Queries, kein hartes Schreiblimit pro Key
Regel: SQL-Queries → D1; einfaches Key-Value mit vielen Reads und wenigen Writes → KV.
Warum kann KV 500µs–10ms Latenz erreichen?
Wozu dient der Parameter cacheTtl?
Wie groß darf ein KV-Value maximal sein?
11 Min. Lesezeit · Veröffentlicht am: 22. Apr. 2026 · Aktualisiert am: 4. Juli 2026
Cloudflare Full Stack
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Astro Cloudflare-Deployment: Kompletter Leitfaden – SSR-Konfiguration + 3× schnellerer Zugriff aus China
Astro von null auf Cloudflare Pages deployen: SSR-Adapter in drei Modi, drei Optimierungsansätze für den Zugriff aus China (Premium-IP, CNAME, regionale DNS-Auflösung) – Latenz in Tests um das Dreifache reduziert
Teil 18 von 23
Nächster
Cloudflare Dynamic Workers: Warum KI-Agent-Sandboxen 100× schneller als Container starten
Cloudflare Dynamic Workers nutzen V8 Isolates für KI-Agent-Sandboxen – Start in Millisekunden statt Sekunden, 10–100× weniger Speicher. Technik, Sicherheit, API-Praxis und Kosten im Vergleich.
Teil 20 von 23
Ähnliche Beiträge
Cloudflare Free Plan Limits 2026: Free, Pro und Business richtig wählen

Cloudflare Free Plan Limits 2026: Free, Pro und Business richtig wählen
Cloudflare Pages: Statischen Blog deployen – 5 Frameworks ohne typische Fehler


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen