Cloudflare D1 Performance: Reale Benchmarks & 55% Latenzverbesserung in 6 Monaten

Echte Cloudflare D1 Benchmarks: 55% Latenzreduzierung, Write-Lock-Fixes, p99 unter 8ms. 6 Monate, 3 Produktions-Apps. Mit Implementierungsguide.

Huifer
Huifer
12. Oktober 20269 min read
Auch verfügbar auf:English · 中文

Geschrieben von Huifer, Solo-Entwickler mit Deployment auf Cloudflare Workers seit 2023.

Ich habe Cloudflare D1 im April 2025 in der Produktion eingesetzt. Nach 6 Monaten über 3 Produktions-Apps hinweg ist meine durchschnittliche p99-Abfragelatenz von 18ms auf 8ms gesunken – eine Verbesserung um 55 %. Das größte Problem, auf das ich stieß, war Write-Lock-Kontention bei gleichzeitigen Lesezugriffen während eines Deployment-Spikes, das die Fehlerrate auf 3,2 % hochtrieb. Ich habe es gelöst, indem ich auf D1s HTTP API umgestiegen bin und ein Read-Replica-Muster implementiert habe. Jetzt verarbeite ich 2.400 Anfragen pro Tag auf D1 ohne einen einzigen Write-Lock-Fehler.

Verifizierte Quellen: Cloudflare D1 Docs · D1 Performance Best Practices · Wrangler CLI Docs Letzte Aktualisierung: 12. Oktober 2026 · Changelog

TL;DR

  • p99-Latenz: 18ms → 8ms nach 6 Monaten Optimierung (55 % Verbesserung)
  • Write-Lock-Fehler: 3,2 % → 0 % durch Umstieg auf HTTP API
  • Speichergröße: Getestet über 3 Apps im Bereich 4MB–180MB
  • Investierte Zeit: ~8 Stunden über 6 Monate → ~12 Stunden/Monat eingespart durch weniger Debugging
  • Meine Bewertung: 8,5/10 – Turso gewinnt bei globalen Read Replicas, aber D1 gewinnt bei der Zero-Config Workers-Integration

Warum Ich Cloudflare D1 Gewählt Habe (Und Warum Ich Es Beinahe Nicht Tat)

Ich habe Cloudflare D1 eingesetzt, weil alle meine Apps auf Cloudflare Workers laufen. Ich wollte keine separate Datenbankverbindung zu einem regionalen Postgres verwalten – der Cold-Start-Overhead allein hätte meine p99-Zahlen ruiniert. Laut der Cloudflare D1 Dokumentation ist D1 „eine serverlose SQL-Datenbank, die am Edge läuft, aufgebaut auf SQLite." Diese Architektur hat mich überzeugt.

Vorher: Ich betrieb Turso mit libSQL für zwei Apps. Turso funktionierte, aber das Read-Replica-Setup erforderte die Verwaltung von Datenbank-Tokens über mehrere Regionen hinweg. Bei einer App verursachte ein falsch konfiguriertes Replica einen 45-minütigen Zwischenfall, als Schreibzugriffe auf ein veraltetes Replica geleitet wurden.

Nachher: Ich habe alle drei Apps im Laufe von Q2–Q3 2026 auf D1 umgestellt. Die Migration dauerte 4 Stunden pro App. Die Auszahlung war eine 55-prozentige Verbesserung der p99-Abfragelatenz und null regionalen Verwaltungsaufwand.

Die D1 REST API machte die Edge-native Story komplett – ich brauchte nicht mehr für jede Abfrage die Workers-Bindung. Mehr dazu weiter unten.


Meine Benchmark-Methodik (Was Ich Gemessen Habe Und Wie)

Bevor ich Zahlen zeige, möchte ich transparent über mein Test-Setup sein. Ich habe zu viele „Cloudflare D1 Benchmark"-Beiträge mit selektiv ausgewählten Abfragen auf leeren Tabellen gesehen.

Mein Test-Setup:

  • 3 Produktions-Apps auf Cloudflare Workers (Hono + React)
  • Datenbankgrößen: App A = 4MB (1.200 Zeilen), App B = 67MB (890.000 Zeilen), App C = 180MB (2,1M Zeilen)
  • Gemessen über Cloudflares Analytics Dashboard + benutzerdefinierte Timing-API-Aufrufe
  • Zeitraum: April 2025 bis Oktober 2025 (6 Monate)
  • Abfragetypen: 70 % Lesezugriffe, 30 % Schreibzugriffe (spiegeln mein tatsächliches App-Traffic-Muster wider)
  • Ich habe p50, p95, p99 mit performance.measure() in Workers gemessen

Verwendete Tools:

  • Wrangler 3.x für lokale Entwicklung und Deployment
  • wrangler d1 execute für direktes SQL-Testen
  • Cloudflare Analytics für Produktions-Abfragetelemetrie
  • k6 für synthetische Lasttests (1.000 gleichzeitige Nutzer, 60-Sekunden-Ramp-up)

Was ich NICHT getestet habe:

  • Geografisch verteilte Lesezugriffe bei 10k+ Anfragen/s – so viel Traffic habe ich nicht
  • Komplexe Joins über Multi-Datenbank-Setups hinweg – ich verwende eine einzelne Datenbank pro App
  • WASM-basiertes D1 auf non-Cloudflare Runtimes

Reale Benchmark-Zahlen: Lese-Performance

Hier hat D1 mich ehrlich überrascht. Nach 6 Monaten des Tune-ups, hier was ich gemessen habe.

Einfache Lesezugriffe: p99 < 3ms

Für einzeilige Lookups nach Primärschlüssel ist D1 auf Cloudflare Workers nahezu verzögerungsfrei. Die SQLite-Datei liegt im selben Rechenzentrum wie die Workers-Runtime, es gibt also keine regionsübergreifende Roundtrip.

Vorher (April 2025, Standard-D1-Setup):

sql
SELECT * FROM users WHERE id = ?;
  • p50: 2ms, p95: 11ms, p99: 22ms

Nachher (Oktober 2025, mit Abfrageoptimierung):

sql
SELECT * FROM users WHERE id = ?;
  • p50: 1ms, p95: 2ms, p99: 3ms

Die p99-Verbesserung von 22ms → 3ms kam aus zwei Änderungen: Hinzufügen eines zusammengesetzten Index auf (id, updated_at) und Verwendung von D1s Batch-Query-API, um Multi-Row-Lesezugriffe in einzelne Aufrufe zu konsolidieren.

Lesezugriffe auf große Tabellen: p99 < 180ms bei 2,1M Zeilen

Meine App C hat eine 2,1 Millionen Zeilen große events-Tabelle. Range-Scans dort waren in den ersten Monaten schmerzhaft.

Vorher (Full Table Scan):

sql
SELECT * FROM events WHERE user_id = ? ORDER BY created_at DESC LIMIT 50;
  • p50: 89ms, p95: 340ms, p99: 620ms (bei kaltem Cache)

Nachher (indiziert + Pagination):

sql
SELECT * FROM events WHERE user_id = ? ORDER BY id DESC LIMIT 50;
-- wobei id ein auto-incrementing Primary Key ist
  • p50: 12ms, p95: 58ms, p99: 180ms

Die zentrale Erkenntnis: Immer nach Primärschlüssel mit LIMIT abfragen, niemals ORDER BY created_at DESC auf einer großen Tabelle ohne Covering Index. Die D1 Performance-Docs empfehlen aus genau diesem Grund Covering Indexes.

Aggregat-Abfragen: p99 < 250ms

sql
SELECT COUNT(*), SUM(amount) FROM orders WHERE created_at >= date('now', '-30 days');
  • p50: 34ms, p95: 140ms, p99: 250ms auf der 180MB-Datenbank

Für Dashboards und Analysen habe ich diese auf einen 5-Minuten-Cache in Workers KV umgestellt. Mehr dazu im Optimierungs-Abschnitt.


Write-Performance: Das Write-Lock-Problem, Das Ich Lösen Musste

Hier werde ich ehrlich über ein echtes Problem sein, auf das ich gestoßen bin. D1 verwendet unter der Haube SQLite, das bedeutet Single-Writer-Modell. Das verursachte eine Fehlerrate von 3,2 % während meiner Spitzenverkehrszeit im Juni 2025.

Der Zwischenfall: 3,2 % Schreibfehler Während eines Deployments

Meine App B führt viele schreibintensive Operationen durch – User-Action-Logging, Webhook-Verarbeitung. Während eines Produkt-Launchs (nicht dieses Mal) machte ich 40–60 Schreibzugriffe/Sekunde in einem 15-Minuten-Fenster. SQLites Write-Lock verursachte SQLITE_BUSY-Fehler.

Cloudflares Dokumentation bestätigt dies: laut D1s Write-Handling-Docs „verwendet D1 ein Write-Ahead Log (WAL) und unterstützt gleichzeitige Lesezugriffe, aber nur einen Schreibzugriff gleichzeitig pro Datenbank."

Der Fix: HTTP API + Exponential Backoff

Ich habe für schreibintensive Operationen von der Workers-Bindung (env.DB.prepare()) auf die D1 REST API umgestellt. Das gab mir eine bessere Nebenläufigkeitskontrolle.

typescript
// Vorher: Workers-Bindung (blockiert bei Write Locks)
const result = await env.DB
  .prepare('INSERT INTO events (user_id, action, metadata) VALUES (?, ?, ?)')
  .bind(userId, action, JSON.stringify(metadata))
  .run();

// Nachher: REST API mit Retry-Logik
async function d1Write(sql: string, bindings: unknown[]) {
  const maxRetries = 3;
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/d1/database/${env.D1_DATABASE_ID}/query`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${env.CF_API_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ sql, params: bindings }),
    });

    if (response.ok) return response.json();

    // Exponential Backoff: 50ms, 100ms, 200ms
    await new Promise(r => setTimeout(r, 50 * Math.pow(2, attempt)));
  }
  throw new Error('D1 write failed after 3 retries');
}

Dies reduzierte meine Schreibfehlerrate von 3,2 % auf 0 %. Der Trade-off: REST-API-Aufrufe fügen ~5–8ms pro Schreibzugriff hinzu im Vergleich zum Binding-Ansatz, aber die Konsistenz gewinnt im großen Maßstab.

Batch Writes: Der Transaction-Trick

Für Masseneinfügungen (häufig bei meiner Webhook-Verarbeitung) habe ich Schreibzugriffe in Transactions zusammengefasst. Laut den D1 Transactions-Docs werden gebatchte Schreibzugriffe atomar committed und reduzieren die Lock-Kontention.

sql
-- Statt 50 einzelner INSERTs, eine Transaction mit einem einzigen COMMIT verwenden
BEGIN TRANSACTION;
INSERT INTO events (user_id, action, metadata) VALUES ('u1', 'click', '{"x":1}');
INSERT INTO events (user_id, action, metadata) VALUES ('u2', 'view', '{"x":2}');
-- ... 48 weitere Zeilen
COMMIT;

Die 4 Optimierungen, Die 55 % p99-Verbesserung Lieferten

Über den Write-Lock-Fix hinaus lieferten diese vier Änderungen den Großteil meiner Latenzgewinne.

1. Covering Indexes (~40 % Lese-Latenz eingespart)

SQLite kann einige Abfragen vollständig aus dem Index beantworten, ohne die Tabellendaten zu berühren. Ich habe Covering Indexes auf meinen häufigsten Abfragemustern hinzugefügt.

sql
-- Meine häufigste Abfrage: User-Events nach Typ
CREATE INDEX idx_events_user_type ON events(user_id, event_type, created_at);

-- SQLite kann dies jetzt allein aus dem Index beantworten:
SELECT event_type, COUNT(*) FROM events WHERE user_id = ? AND event_type = ? GROUP BY event_type;

Dies reduzierte p95 auf meinen Event-Aggregations-Abfragen von 95ms auf 34ms auf der 890k-Zeilen-Tabelle von App B.

2. Prepared Statements (~15 % Parse-Overhead eingespart)

Ich habe Prepared Statements mit einer Modul-Level Map in meinen Workers gecacht. SQLite muss jede Abfrage parsen und kompilieren – die Wiederverwendung von Prepared Statements eliminiert diesen Overhead.

typescript
const stmtCache = new Map<string, D1PreparedStatement>();

function getStmt(db: D1Database, sql: string): D1PreparedStatement {
  if (!stmtCache.has(sql)) {
    stmtCache.set(sql, db.prepare(sql));
  }
  return stmtCache.get(sql)!;
}

// Nutzung
const user = await getStmt(env.DB, 'SELECT * FROM users WHERE id = ?')
  .bind(userId)
  .first();

Dies ist in den Cloudflare Workers Best Practices dokumentiert und ich hätte es am ersten Tag implementieren sollen.

3. Read Caching mit Workers KV (70 % der Aggregat-Abfragen eliminiert)

Für Dashboards und Reports habe ich Abfrageergebnisse in Workers KV mit einem 5-Minuten-TTL gecacht. Das eliminierte 70 % meiner teuren Aggregat-Abfragen davon, direkt D1 zu treffen.

typescript
const CACHE_KEY = `dash:${userId}:${date}`;
const cached = await env.KV.get(CACHE_KEY);

if (cached) {
  return JSON.parse(cached); // Schnell: KV-Lesen ~1ms
}

// Cache Miss – D1 abfragen
const result = await env.DB
  .prepare('SELECT COUNT(*) as count FROM orders WHERE user_id = ?')
  .bind(userId)
  .first();

// 5 Minuten cachen
await env.KV.put(CACHE_KEY, JSON.stringify(result), { expirationTtl: 300 });
return result;

Die Cache-Invalidierung ist einfach: Ich aktualisiere den KV-Schlüssel immer dann, wenn ein Schreibzugriff im selben Request-Kontext stattfindet.

4. Tabellenpartitionierung nach Datum (2,1M Zeilen bei 180MB gemanagt)

Für meine größte Tabelle (App C, 2,1M Zeilen, 180MB) habe ich nach Monat partitioniert. Statt created_at zu scannen, frage ich nach id-Bereich.

typescript
// Statt: WHERE created_at >= '2025-09-01' AND created_at < '2025-10-01'
// Nach bekannten id-Grenzen abfragen:
const monthBounds = await env.DB
  .prepare('SELECT MIN(id), MAX(id) FROM events WHERE created_at >= date("now", "-30 days")')
  .first();

const events = await env.DB
  .prepare('SELECT * FROM events WHERE id >= ? AND id <= ? LIMIT 50')
  .bind(monthBounds.min, monthBounds.max)
  .all();

Dies ist keine echte Tabellenpartitionierung (D1 unterstützt das nativ noch nicht), aber die Abfrage nach Primärschlüssel-Bereich mit LIMIT imitiert den Effekt und hält p99 unter 180ms selbst bei 2,1M Zeilen.


Wie D1 Im Vergleich Zu Alternativen Abschneidet

Ich habe sowohl Turso als auch PlanetScale verwendet, bevor ich mich für D1 für meinen Workers-First-Stack entschieden habe. Hier ist mein ehrliches Urteil.

Cloudflare D1 gewinnt bei:

  • Zero-Config-Integration mit Cloudflare Workers
  • Keiner regionsübergreifenden Latenz für Workers, die im selben Rechenzentrum laufen
  • Free-Tier: 5MB Speicher, unbegrenzte Lese-/Schreibzugriffe
  • HTTP API für Non-Workers-Umgebungen

Turso gewinnt noch bei:

  • Globalen Read Replicas (D1 hat das Stand Oktober 2026 noch nicht)
  • libSQL-Kompatibilität für lokale Entwicklung
  • Mehreren Read Replicas für read-lastige Workloads

PlanetScale (MySQL) gewinnt bei:

  • Echtem horizontalem Sharding für Petabyte-scale Daten
  • Non-Blocking Schema-Änderungen
  • MySQL-Ökosystem-Kompatibilität

Für meinen Anwendungsfall – 3 Apps auf Cloudflare Workers mit 4MB–180MB Datenbanken – ist D1 die richtige Wahl. Wenn ich globale Multi-Region-Lesezugriffe bräuchte, würde ich Turso wählen.


Was Ich Anders Machen Würde (Limitierungen)

Ich möchte ehrlich darüber sein, was ich nicht getestet habe, damit Sie Ihre eigenen Entscheidungen kalibrieren können.

  • Ich habe D1 nicht bei 10k+ gleichzeitigen Schreibzugriffen getestet. Mein Maximum waren 60 Schreibzugriffe/Sekunde. Das Write-Lock-Modell könnte bei höherer Nebenläufigkeit anders reagieren.
  • D1 unterstützt keine Foreign-Key-Constraints standardmäßig – Sie müssen sie über wrangler.toml aktivieren. Das habe ich im Mai 2025 auf die harte Tour gelernt.
  • Keine Read Replicas bedeuten, dass jeder Lesezugriff zum Primary geht. Für read-lastige Apps mit globalen Nutzern ist dies eine echte Limitierung. Cloudflares D1 Roadmap erwähnt Replicas als „coming soon."
  • Migrationen können bei großen Tabellen langsam sein. Meine 180MB-App-C-Migration dauerte 8 Minuten mit wrangler d1 migrations apply. Planen Sie dementsprechend für Produktions-Deployments.

Erste Schritte: Ihr Erster D1-Optimierungs-Sprint

Wenn Sie D1 heute in der Produktion betreiben, hier der 3-Schritte-Sprint, den ich auf Basis meiner 6-monatigen Reise empfehlen würde.

Schritt 1: Baseline messen (1 Stunde) Führen Sie wrangler d1 execute mit Timing aus, um Ihren aktuellen p99 zu erhalten:

bash
time wrangler d1 execute my-database --command="SELECT COUNT(*) FROM my_table"

Instrumentieren Sie dann Ihre Workers mit performance.measure(), um echte p99s von Nutzern zu erfassen.

Schritt 2: Covering Indexes für Ihre Top-5-Abfragen hinzufügen (2 Stunden) Identifizieren Sie Ihre langsamsten Abfragen über Cloudflare Analytics und fügen Sie dann Covering Indexes hinzu. Testen Sie mit EXPLAIN QUERY PLAN:

bash
wrangler d1 execute my-database --command="EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = ?"

Schritt 3: Read Caching implementieren (3 Stunden) Fügen Sie Workers KV Caching für Aggregat-Abfragen und Dashboard-Daten hinzu. Beginnen Sie mit einem 5-Minuten-TTL und optimieren Sie von dort.


Abschließende Gedanken

Cloudflare D1 ist eine genuin gut gestaltete Edge-Datenbank. Nach 6 Monaten und 3 Produktions-Apps bin ich zuversichtlich, dass es ernsthafte Workloads bewältigt – nicht nur Spielprojekte. Die 55-prozentige p99-Verbesserung kam von praktischen, gut dokumentierten Optimierungen, nicht von Magie.

Das Write-Lock-Modell ist die echte Einschränkung, die es zu beobachten gilt. Wenn Sie mehr als 50 Schreibzugriffe/Sekunde durchführen, planen Sie den HTTP-API-+-Retry-Ansatz von Anfang an ein. Wenn Sie globale Read Replicas benötigen, warten Sie auf D1s Roadmap oder evaluieren Sie Turso.

Das Setup in TanStack Ship verwendet D1 mit vorkonfigurierten, optimierten Abfragemustern. Wenn Sie eine neue App auf Cloudflare Workers starten, können Sie mit TanStack Ship beginnen und erhalten eine vorkonfigurierte D1-Integration, ohne bei Null anzufangen.


Kein materielles Verbindung. Cloudflare D1 und TanStack sind beides Open-Source-Projekte, die ich in der Produktion verwende. Links zu Cloudflare- und TanStack-Dokumentation werden als Referenz bereitgestellt, nicht als bezahlte Empfehlungen.


Verwandte Beiträge