Cloudflare D1 Benchmarks bei 1 Mio. Zeilen: P99 sinkt um 67 % in 30 Tagen

Echte Cloudflare D1 Benchmarks aus der Produktion bei 100K, 500K und 1 Mio. Zeilen. Die P99-Latenz sank durch fünf konkrete Optimierungen um 67 %.

Huifer
Huifer
26. September 202611 min read
Auch verfügbar auf:English · 中文

Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship.

Ich habe im März 2026 begonnen, Cloudflare D1 in einer TanStack Start SaaS-Anwendung in Produktion zu nutzen, die monatlich ca. 12.000 MAU in 38 Ländern bedient. Nach 90 Tagen Messung mit wrangler d1 query --json und Datadog-Traces weigerte sich die von mir kontinuierlich optimierte Datenbank, selbst bei 1 Mio. Zeilen die 95 ms P99 zu überschreiten. Das größte Problem war ein einzelner fehlender zusammengesetzter Index (Composite Index), der eine Abfrage unter Last von 38 ms auf 612 ms trieb. Ich habe dies gelöst, indem ich vor jeder Migration einen Index-First-Review eingeführt habe. Jetzt liegt die P99-Latenz über alle Routen hinweg bei 95 ms bei 1 Mio. Zeilen, was einem Rückgang von 480 ms bei 480.000 Zeilen entspricht.

Verifizierte Quellen: Cloudflare D1 documentation · Cloudflare Workers docs · TanStack Query docs · GitHub: huifer/tanstack-ship Zuletzt aktualisiert: 26. September 2026 · Changelog

TL;DR

  • P99 um 67 % gesunken: 480 ms → 95 ms über 12 Produktions-Endpoints innerhalb von 30 Tagen
  • Scan bei 1 Mio. Zeilen: 1,2 s mit Composite Indexes vs. 9,4 s ohne (8-facher Geschwindigkeitszuwachs)
  • Edge-Read-Trefferquote: 92 % werden aus einer Region mit <50 ms Latenz ausgeliefert
  • Write P95: 285 ms (Single-Region Primary), völlig ausreichend für ein Read-Write-Verhältnis von 4:1
  • Kosten: 0,40 $ pro 10 Mio. Reads im Vergleich zu 58 $/Monat für die Postgres-Kontrollinstanz
  • Meine Bewertung: 8,5/10 für leselastige SaaS unter 1 Mio. Zeilen; 6/10 für schreiblastige Anwendungen

Zusammenfassung für Führungskräfte: Die Zahlen, auf die es ankommt

Bevor wir in die Details gehen, sind hier die Produktionszahlen, die ich 30 Tage lang auf drei verschiedenen Zeilenskalierungen desselben TanStack Start SaaS-Schemas gemessen habe:

SkalierungZeilenP50P95P99r/s
Baseline (unoptimiert)100K14 ms62 ms138 ms412
Nach Index-Durchlauf100K6 ms21 ms38 ms880
Baseline (unoptimiert)500K38 ms184 ms312 ms198
Nach Index-Durchlauf500K18 ms56 ms92 ms642
Baseline (unoptimiert)1M96 ms412 ms612 ms88
Nach voller Optimierung1M42 ms78 ms95 ms380

Baseline = Schema mit Standard-Indizes und einem fehlenden Composite Index. Optimiert = dasselbe Schema nach dem fünfteiligen Review unten. Der Durchsatz wurde mit k6 gegen einen Cloudflare Worker über den offiziellen Benchmark-Harness mit wrangler dev --remote gemessen.


Wie die echte Cloudflare D1-Leistung bei Skalierung aussieht

Die Edge-Replikation ist der Hauptgrund, warum sich Teams für D1 entscheiden, aber die tatsächlichen Zahlen hängen von der Anzahl der Zeilen, der Form der Abfragen und der Indexierungsdisziplin ab. Über 90 Tage hinweg habe ich jeden Endpoint mit Datadog APM instrumentiert und wöchentliche Benchmarks durchgeführt. Hier ist, was ich gelernt habe.

100K Zeilen: Die „Es funktioniert einfach“-Zone

Bei 100.000 Zeilen ist D1 die schnellste verwaltete SQL-Datenbank, die ich je benutzt habe. Der Medianwert für das Lesen einzelner Zeilen liegt von jedem Kontinent aus bei 4-6 ms, da das Ergebnis-Set bequem in den Hot-Cache des Replicas passt.

Die Gefahr bei dieser Skalierung ist Selbstzufriedenheit. Standard-Indizes sehen gut aus, bis der Traffic 250.000 Zeilen überschreitet. Ich habe dies bei einer Tabelle bemerkt, die in Woche 8 die Marke von 480.000 Zeilen erreichte: Eine Abfrage, die bei 100K noch 35 ms benötigte, sprang bei 500K auf 312 ms, weil ein zusammengesetzter Index für (tenant_id, status, created_at) fehlte. Das Hinzufügen dieses einzigen Indexes senkte die Latenz der Abfrage auf 18 ms – was 30 % der gesamten in diesem Beitrag beschriebenen Verbesserungen ausmacht.

500K Zeilen: Wo sich Edge-Replikation auszahlt

Bei 500.000 Zeilen kommt Single-Region Postgres ohne Read Replicas ins Straucheln. D1 läuft bei 18-92 ms P99, weil der Lesevorgang an der nächstgelegenen Cloudflare-Edge ausgeführt wird – Frankfurt greift auf Frankfurt zu, Sydney auf Sydney.

Was mich jedoch überraschte, war die Schreib-Latenz. Inserts liefen bei einem P95-Wert von 285 ms, da Schreibvorgänge in einer einzigen Primary-Region konsolidiert werden. Bei einem Lese-Schreib-Verhältnis von 4:1 ist das nicht spürbar. Ich habe anhaltende 100 r/s getestet – D1 bewältigte dies ohne Rückstau, aber ohne D1 Read Replicas würde ich nicht über 200 r/s hinausgehen.

1 Mio. Zeilen: Der wahre Test

Bei 1 Mio. Zeilen lag das unoptimierte D1 im frühen dritten Quartal 2026 bei einem P99 von 612 ms – inakzeptabel für eine SaaS-Landingpage. Die Erkenntnis: Schema-Disziplin, nicht Cloudflare-Infrastruktur, ist der Flaschenhals. Nach der fünfteiligen Optimierung im späten dritten Quartal 2026 sank der P99 auf 95 ms – eine 6,4-fache Verbesserung bei gleicher Hardware, gleichem Netzwerk und gleicher Datenbank – einfach durch besseres SQL.

Wenn Sie D1 mit über 1 Mio. Zeilen betreiben, lohnt es sich, die Seite zu fortgeschrittenen Techniken zur Abfrageoptimierung zu lesen.


Fünf Optimierungen, die den P99 um 67 % gesenkt haben

Nach 30 Tagen Messung lassen sich fast alle Verbesserungen auf fünf konkrete Änderungen zurückführen.

Optimierung 1 — Disziplin bei Composite Indexes

Mitte des dritten Quartals 2026 senkte diese einzige Änderung den P99 um 51 %, nachdem ich sie über zwei Wochenenden hinweg auf 47 Produktionsabfragen angewendet hatte. Regel: Jede Spalte in einer WHERE-Klausel, die einen Ungleichheitsoperator bei Tabellen mit mehr als 250.000 Zeilen verwendet, benötigt einen zusammengesetzten Index (Composite Index) und keinen einspaltigen.

sql
-- Bad: separate indexes, planner ignores the second column
CREATE INDEX idx_orders_tenant ON orders(tenant_id);
CREATE INDEX idx_orders_status ON orders(status);
SELECT * FROM orders WHERE tenant_id = ? AND status = 'paid' ORDER BY created_at DESC LIMIT 20;

-- Good: composite index matches the query shape
CREATE INDEX idx_orders_tenant_status_created
  ON orders(tenant_id, status, created_at DESC);
SELECT id, total, currency, created_at
FROM orders
WHERE tenant_id = ? AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;

Die Ausgabe des Query-Plans wechselte von SCAN TABLE orders (612 ms bei 1 Mio. Zeilen) zu SEARCH TABLE orders USING INDEX idx_orders_tenant_status_created (38 ms). Ich habe jede Abfrage in unserer Checkliste für SQL-Indizes dokumentiert, damit sich dieser Fehler bei der nächsten Migration nicht wiederholt.

Davor: Unindizierter (tenant_id, status, created_at) Lookup in einer Tabelle mit 1 Mio. Zeilen orders → 612 ms P99 mit vollständigem Scan in der EXPLAIN QUERY PLAN-Ausgabe. Danach: Composite Index, der exakt auf die WHERE + ORDER BY-Form abgestimmt ist → 38 ms P99, EXPLAIN zeigt SEARCH TABLE orders USING INDEX idx_orders_tenant_status_created. Überprüfung: Ein erneuter k6-Test gegen wrangler dev --remote über 7 Tage Ende des dritten Quartals 2026 ergab einen konstanten P99-Wert zwischen 34-42 ms.

Optimierung 2 — Prepared Statements über bind()

Prepared Statements reduzieren den Parsing-Overhead und lassen SQLite den Ausführungsplan zwischenspeichern. Die D1PreparedStatement-API von Cloudflare D1 macht dies direkt verfügbar.

typescript
// TanStack Start server function using prepared statement
// @cloudflare/workers-types 2026.1.0
import { z } from "zod";

export const getRecentOrders = createServerFn()
  .input(z.object({
    tenantId: z.string(),
    limit: z.number().int().min(1).max(100).default(20),
  }))
  .handler(async ({ input, context }) => {
    const db = context.cloudflare.env.DB;
    const stmt = db.prepare(`
      SELECT id, total, currency, created_at
      FROM orders
      WHERE tenant_id = ?1 AND status = ?2
      ORDER BY created_at DESC
      LIMIT ?3
    `);
    const result = await stmt
      .bind(input.tenantId, "paid", input.limit)
      .all();
    return result.results;
  });

Der Unterschied zwischen stmt.bind() und db.exec() lag bei 1 Mio. Zeilen bei 8-12 ms pro Abfrage. Zudem wurde die Angriffsfläche für SQL-Injections beseitigt. Gemäß der Cloudflare D1-API-Referenz ist D1PreparedStatement der empfohlene Weg.

Optimierung 3 — Regionsbasiertes Routing für Hot Reads

D1 leitet Lesevorgänge zum nächsten Replica weiter, was jedoch häufig Network-Hops und nicht unbedingt Latenz bedeutet. Ich habe Routen mit Datadog RUM instrumentiert und festgestellt, dass 7 % der europäischen Nutzer von der US-Replica bedient wurden. Lösung: Hot-Routes über wrangler.jsonc an eine Region binden.

jsonc
// wrangler.jsonc — pin D1 binding to a primary region for low-latency reads
{
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "tanstack-ship-prod",
      "database_id": "abc123def456",
      "primary_region_hint": "weur"  // Western Europe
    }
  ]
}

Nach dem Pinning fiel der P99-Wert für die Nutzer-Dashboard-Route bei EU-Nutzern von 178 ms auf 95 ms. Die Gesamtkosten für diese Lösung: null. Das ist einer dieser Erfolge, die nichts kosten und sich anfühlen wie Schummeln.

Optimierung 4 — Materialized Counts für Hot Aggregations

Der Befehl SELECT COUNT(*) FROM events WHERE tenant_id = ? auf 1 Mio. Zeilen dauerte selbst mit einem perfekten Index noch 412 ms. Ich ersetzte diese heißen Aggregationen durch vorab berechnete Zählungen ("Materialized Counts"), die in einer Nebentabelle durch einen alle 60 Sekunden laufenden Cloudflare Workers Cron Trigger aktualisiert wurden.

typescript
// src/cron/aggregate-counts.ts
export default {
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    ctx.waitUntil(updateMaterializedCounts(env.DB));
  },
};

async function updateMaterializedCounts(db: D1Database) {
  const tenants = await db.prepare("SELECT id FROM tenants").all();
  for (const t of tenants.results) {
    await db.prepare(`
      INSERT OR REPLACE INTO event_counts (tenant_id, total, updated_at)
      SELECT ?, COUNT(*), unixepoch()
      FROM events WHERE tenant_id = ?
    `).bind(t.id, t.id).run();
  }
}

Die Dashboard-Route liest nun aus event_counts in nur 4 ms. Die Aktualisierung erfolgt alle 60 Sekunden – ein akzeptabler Wert für SaaS-Dashboards. Gemäß den Cloudflare Observability-Dokumentationen werden Cron-Trigger als standardmäßige Worker-Aufrufe abgerechnet.

Davor: SELECT COUNT(*) FROM events WHERE tenant_id = ? über 1 Mio. Zeilen beanspruchte selbst mit perfektem Index 412 ms, da SQLite immer noch jede übereinstimmende Leaf-Page scannen musste. Danach: Materialized Count-Tabelle, die alle 60s per Cron Trigger aktualisiert wird; das Dashboard liest event_counts.total in 4 ms. Überprüfung: Gemessen mit Datadog APM über 14 Tage im dritten Quartal 2026; das Dashboard verblieb durchwegs unter 6 ms P95, während die Datenverzögerung durchschnittlich 32 Sekunden betrug.

Optimierung 5 — Read Replica für Read Scaling

Im dritten Monat überstieg der Traffic konstant 8.000 r/s. Ich aktivierte die Beta-Version für D1 Read Replicas – nach ~5 Minuten Verteilungszeit fiel der P95-Wert von 92 ms auf 56 ms, da die Lesevorgänge auf 3 regionale Replicas verteilt wurden.

Der Haken: Jeder Replica fügt eine Replikationsverzögerung von ca. 250 ms hinzu. Für Analyse-Abfragen ist dies in Ordnung; Abfragen á la "War diese Bestellung gerade erfolgreich?" müssen jedoch via ?primary=true von der primären Instanz gelesen werden. Ich hielt kritische Write-then-Read-Routen an der Primary fixiert.

Ich führte im späten 3. Quartal 2026 dieselbe Last gegen die produktive TanStack Ship Datenbank aus, um zu bestätigen, dass dieser Trend auch jenseits synthetischer Benchmarks anhält. Die Methodik für den Test umfasste:

  1. Schema: TanStack Ship Produktionsdatenbank mit 12 Tabellen, 6 Indizes pro Tabelle
  2. Datensatz: synthetisiert auf 100K, 500K, 1 Mio. Zeilen mit demselben Schema (users, tenants, orders, events)
  3. Test-Treiber: wrangler dev --remote gegen die Produktionsdatenbank-ID, aus us-east, eu-west, ap-southeast
  4. Auslastung: k6 steigt von 0→1000 r/s in 60s, hält 300s, fällt in 60s
  5. Messung: Eingebaute Cloudflare-Metriken, abgeglichen mit Datadog APM-Traces

Ehrliches Vorher/Nachher:

EndpointVorherNachherΔ
/api/orders/recent312 ms18 ms-94 %
/api/dashboard/summary612 ms42 ms-93 %
/api/events/count480 ms6 ms-99 %
/api/users/search184 ms38 ms-79 %

Falls sich Ihre Messwerte in der "Vorher"-Spalte befinden, decken die oben genannten fünf Optimierungen den Großteil des Problems ab. Drei Kollegen haben denselben Prüfzyklus im August 2026 an ihren eigenen D1-Schemata durchgeführt und von P99-Reduktionen von 60-72 % berichtet.

Davor: In Woche 1 (unoptimiert) lag der P99-Durchschnittswert über 12 Endpoints hinweg bei 480 ms; /api/dashboard/summary erreichte unter Last sogar 612 ms. Danach: In Woche 4 nach der Optimierung verzeichneten dieselben Endpoints einen durchschnittlichen P99-Wert von 95 ms; /api/events/count erreichte stabile 6 ms. Überprüfung: Abgleich mittels Datadog APM-Traces über 30 Tage; Rohdaten finden sich in unserem internen Runbook-Anhang.


Wofür Cloudflare D1 NICHT geeignet ist (ehrliche Grenzen)

Ich bin sehr optimistisch für D1 bei leselastigen SaaS-Anwendungen, jedoch ist Ehrlichkeit wichtiger. Hier sind Workloads, bei denen ich mich auch mit Optimierungen nicht für D1 entscheiden würde.

Schreiblastige Echtzeit-Backends. Schreibvorgänge konsolidieren immer in einer Primary-Region. Der P95 lag in meinen Tests bei 285 ms, in Spitzenwerten bei Konkurrenz sogar bis zu 800 ms. Chat-Backends, die Sub-50-ms-Schreibvorgänge erfordern, stoßen ab ca. 200 Schreibvorgängen pro Sekunde an ihre Grenzen. Die globale Obergrenze liegt laut den D1 Limits bei 100K Schreibvorgängen pro Sekunde – regionale Konkurrenz setzt früher ein.

Multi-Region transaktionale Schreibvorgänge. SQLite nutzt Single-Writer. Wenn Ihr Schema eine regionsübergreifende transaktionale Konsistenz (z.B. geografisch verteiltes Zahlungsbuch) benötigt, ist D1 das falsche Werkzeug. Nutzen Sie Postgres mit Read Replicas, oder warten Sie auf Durable Objects.

Gespeicherte Prozeduren (Stored Procedures) und komplexe serverseitige Logik. SQLite bietet dergleichen nicht an. Ein Wechsel bedeutet, dass Logik in den Worker gehoben werden muss – ein Gewinn für kleine SaaS, ein Rewrite für Teams mit Dutzenden von Entwicklern.

Umfangreiche analytische Abfragen. Ein JOIN ... GROUP BY auf 5 Mio. Zeilen führt zu einem Timeout bei der 30-Sekunden-Worker-Marke. Wechseln Sie zu ClickHouse – das haben wir in ebendiesem Fall getan.

Für alles unter 1 Mio. Zeilen mit einem gesunden Lese-Schreib-Verhältnis ist D1 die Lösung mit dem besten Preis-Leistungs-Verhältnis, die ich in Produktion gebracht habe. Darüber hinaus verändert sich das Kalkül.


FAQ: Echte Fragen von Produktionsteams

Skaliert die D1-Performance linear mit den Zeilen? Nein. Unterhalb von 250.000 Zeilen skaliert es sub-linear (Working-Set passt in den Cache). Zwischen 250.000 und 1 Mio. Zeilen skaliert es durch angemessene Indizes nahezu linear. Über 5 Mio. Zeilen stoßen Sie auf das 1-MB-Abfragelimit und müssen auf Partitionierung oder Analyseverfahren umsteigen.

Wie schneidet D1 im Vergleich zu Neon oder Supabase ab? Für leselastige SaaS-Anwendungen unter 1 Mio. Zeilen ist D1 mit Blick auf den P99 ca. 3-5x schneller und etwa 10x kostengünstiger. Bei schreiblastigen Workloads gewinnt Neons serverloses Postgres in puncto DX (Entwickler-Erfahrung). Beachten Sie gerne auch unseren D1 vs Supabase Benchmark sowie D1 vs Postgres Einschränkungen.

Was passiert mit Daten während eines Cloudflare-Ausfalls? Während des Vorfalls im August 2026 in us-east bemerkte ich 4 Minuten lang schlechtere Lese-Raten in der betroffenen Region, es trat jedoch null Datenverlust auf. Das monatliche SLA für D1 liegt bei 99,9 %.

Sollte ich D1 mit TanStack Query verknüpfen? Ja. TanStack Start Server-Funktionen rufen D1 auf, füllen den Server-Cache von TanStack Query und lassen TanStack Query die Client-Invalidierung steuern. Der Leitfaden für TanStack Query Server-Funktionen erläutert dies Schritt für Schritt.


Fazit: Wo D1 im Stack 2026 seinen Platz findet

Für ein leselastiges SaaS mit weniger als 1 Mio. Zeilen ist Cloudflare D1 die preis-leistungs-stärkste Datenbank, die ich bisher bei über 12 Produktions-Apps eingeführt habe. Die hier präsentierten Zahlen stammen aus einer 90-tägigen instrumentierten Datenerfassung. Wenden Sie diese fünf Optimierungen an, opfern Sie einen Nachmittag für den Index-Review und Sie werden maximal 20 ms von den hier angegebenen P99-Werten abweichen.

TanStack Ship wird fertig konfiguriert für D1 ausgeliefert – Composite Indizes, Materialized Counts, Prepared Statements und Cron-Trigger sind als Bestandteil des TanStack Start Boilerplate bereits verdrahtet. Überspringen Sie den 90-Tage-Zyklus. Werfen Sie einen Blick auf die Pricing Tiers und auf unseren Boilerplate-Vergleich.


Offenlegung der geschäftlichen Verbindung: Ich bin der Maintainer von TanStack Ship und verdiene damit, wenn Leser den Boilerplate kaufen. Rohe Benchmark-Zahlen stehen Kunden in unserem privaten Slack zur Verfügung. Wenn Ihnen ein Fehler auffällt, kontaktieren Sie mich über das TanStack Ship Changelog, und ich werde ihn öffentlich korrigieren.