Cloudflare D1 vs Supabase: Echte Benchmarks bei 100.000 Zeilen

Lese-/Schreibgeschwindigkeiten, Preise und DX-Vergleich. Wann Cloudflare D1 besser als Supabase ist. Echte Benchmark-Daten aus unseren Tests für SaaS.

Huifer
Huifer
22. August 202611 min read
Auch verfügbar auf:English · 中文

Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship. Ich betreibe Cloudflare D1 seit achtzehn Monaten in acht produktiven SaaS-Apps – ein Multi-Tenant Analytics Tool mit ~140.000 Anfragen/Tag, eine B2B-Abrechnungsplattform mit ~3.200 zahlenden Kunden, ein Developer Dashboard, zwei B2B-Marktplätze und drei kleinere Apps. Dieser Artikel konsolidiert die tatsächlichen Leistungsdaten, die ich gemessen habe, die Muster, die sich bewähren, und die Klippen, die eine SaaS-App bei Skalierung in die Knie zwingen.

Verifizierte Quellen: Cloudflare D1 Dokumentation · D1 Best Practices · D1 Query API · D1 Read Replicas · D1 Limits · SQLite Query Planner · Workers Analytics Engine · TanStack Ship D1 Referenz

Zuletzt aktualisiert: 22. August 2026 · Changelog


TL;DR: Cloudflare D1 ist das richtige Primitiv für leselastige SaaS an der Edge – Sub-Millisekunden-Lesezugriffe vom nächsten Replica, null Kosten für Connection-Pools, vorhersehbare SQLite-Pläne. Es ist das falsche Primitiv für schreiblastige Single-Row-Pfade bei Lastspitzen: Der Write-Lock auf Datenbankebene serialisiert jede Mutation, und die p99-Latenz steigt ab ~200 Mutationen pro Sekunde stark an. Über acht produktive Apps hinweg habe ich p50 Lesezugriffe bei 0,8–1,4 ms (regional), p95 Lesezugriffe bei 2–6 ms und eine Write-Lock-Obergrenze von rund 250 req/s gemessen, bevor Timeouts auftreten. Die Muster, die Cloudflare D1 auf SaaS-Standards halten, sind Composite Indexes mit tenant_id an erster Stelle, db.batch() für transaktionale Multi-Row-Schreibvorgänge und Cloudflare Queues für den Write Fan-out. Wenn Sie sich für einen Stack entscheiden, lesen Sie die TanStack Ship Features-Seite; für die allgemeinere Entscheidung zu Edge-Datenbanken siehe den Cloudflare D1 vs. Alternativen-Vergleich.


Cloudflare D1 Performance für SaaS: Was mich 18 Monate in Produktion gelehrt haben

Die Performance einer SaaS-Datenbank ist nicht nur eine einzelne Zahl. Sie ist ein Vektor – Lese-Latenz beim p99, Schreibdurchsatz bei Lastspitzen, Replikationsverzögerung zwischen Regionen und die Frage, ob Ihre App reaktionsfähig bleibt, wenn der Traffic am Launch-Tag um das Zehnfache ansteigt. Cloudflare D1 ist auf einigen dieser Achsen exzellent und auf anderen unerbittlich. Dieser Artikel bietet die ehrliche Messung, die bewährten Muster und die Klippen, für die Sie planen sollten.

Was „Performance“ bei einer SaaS-Datenbank tatsächlich bedeutet

Wenn Gründer mich fragen: „Ist Cloudflare D1 schnell genug?“, ist diese Frage meistens unterspezifiziert. Es gibt mindestens vier Performance-Dimensionen, die für eine echte SaaS relevant sind, und D1 schneidet in jeder davon sehr unterschiedlich ab.

Lese-Latenz an der regionalen Edge ist die stärkste Achse von Cloudflare D1. Ein Primary-Key-Lookup, der vom nächstgelegenen Replica bedient wird, antwortet in meinen Traces in unter 1,5 ms. Für ein SaaS-Dashboard, eine Auth-Prüfung oder eine Projektliste ist diese Latenz für den Benutzer unsichtbar.

Schreibdurchsatz beim Datenbank-Lock ist die schärfste Klippe von Cloudflare D1. Jede Mutation (Änderung) wird hinter einem einzigen Write-Lock für die Dauer der Transaktion serialisiert. Bei meiner am stärksten ausgelasteten Datenbank – dem Abrechnungsmodul mit ~3.200 Kunden – beginnt die Klippe bei etwa 200 req/s und geht bei 250 req/s in 30 % Request-Timeouts über. Dieser Vorfall ist im D1 Write-Lock Postmortem dokumentiert.

Read-after-Write Konsistenz ist die Dimension, vor der Sie niemand warnt, bis Sie darauf stoßen. Ein Schreibvorgang auf den Primary in ENAM ist laut der D1 Read Replicas Dokumentation typischerweise in einem Zeitfenster von unter 5 Sekunden in einem WEU-Replica sichtbar, aber „typischerweise“ ist nicht „deterministisch“.

Tail Behavior (Ausreißerverhalten) ist das, was den Bereitschaftsdienst alarmiert. Die Median-Latenz ist nur eine Marketing-Zahl; p99 ist das, was zählt. Ich habe gemessen, wie die p99-Schreiblatenz während einer anhaltenden Lastspitze von ~120 ms im Ruhezustand auf über 8 Sekunden anstieg – eine 60-fache Verschlechterung, die unsichtbar bleibt, bis die Spitze eintritt.

Der Rest dieses Artikels widmet sich den tatsächlichen Messwerten entlang dieser vier Achsen sowie den Mustern, die Cloudflare D1 innerhalb der Grenzen halten, in denen SaaS-Performance akzeptabel ist.

Was ich in acht produktiven Apps tatsächlich messe

Ich führe hier keine künstlichen Benchmarks durch; ich berichte Zahlen aus echten Workers-Traces, die über achtzehn Monate hinweg in die Workers Analytics Engine geschrieben wurden. Die Methodik ist simpel: Jeder Datenbankaufruf ist in einen schmalen Telemetrie-Helper verpackt, der den SQL-Hash, die verstrichene Zeit und das Binding aufzeichnet. Percentile-Abfragen gegen die Analytics Engine liefern die Latenzverteilung pro Abfrageform.

Lese-Latenz an der regionalen Edge

In allen acht Apps antworten Primary-Key-Lesezugriffe (SELECT ... WHERE id = ?) auf häufig genutzte Entitäten – Benutzer, Projekte, Abonnements – in der folgenden Verteilung, wenn sie von einem regionalen Replica bedient werden:

PercentileLatenzAnmerkungen
p500,8–1,4 msNächstgelegenes Replica
p952,0–6,0 msCross-Region Replicas
p998–18 msCold-Path Binding-Initialisierung

Das Muster, das mich am meisten überrascht hat, ist, wie flach die Verteilung bei einer Mandantenpopulation in einer einzelnen Region ist. Mein Analytics-Tool generiert ~92 % seines Traffics aus Nordamerika; der p99 liegt dort bei 8 ms. Mein B2B-Marktplatz, der in Europa abgerechnet wird, hat einen p99 von 14 ms, wobei dieser „Long Tail“ von Anfragen dominiert wird, die den Ozean überqueren. Die genauere Anleitung zur Abfrageoptimierung finden Sie im D1 Optimization Writeup.

Schreibdurchsatz beim Datenbank-Lock

Einzelzeilen-INSERT- und UPDATE-Anweisungen auf der am stärksten ausgelasteten Datenbank zeigen ein viel engeres Fenster. Jede Anweisung fordert den Write-Lock für die Dauer der Transaktion an; ein kontinuierlicher Datenstrom reiht sich dahinter ein.

sql
-- Die Art von Abfrage, auf die sich die Obergrenze bezieht
INSERT INTO usage_events (id, tenant_id, kind, payload, created_at)
VALUES (?, ?, ?, ?, unixepoch());

UPDATE tenants SET balance_cents = balance_cents - ?
WHERE id = ?;

Gemessen im Dauerbetrieb (Steady-State) auf der Abrechnungsdatenbank, bevor der auf Queues basierende Writer live ging:

PercentileLatenzAnmerkungen
p5035 msEinzelne Anweisung, keine Konflikte (Contention)
p95140 msLeichte Konflikte durch Cron-Jobs
p991,2 sPhasen mit Lastspitzen (Bursty)
Timeouts~2 %Bei ~180 Mutationen/s

Ab etwa 250 req/s bricht das System zusammen: Die p99-Latenz klettert über 8 s und Timeouts übersteigen die 30 %-Marke. Es gibt hier keine elegante Degradation (Graceful Degradation), sondern nur eine harte Klippe. Den Fix dazu beschreibe ich im D1 Write-Lock Postmortem, und er ist nun das Standard-Abrechnungsmodul in TanStack Ship.

p99-Ausreißerverhalten bei Lastspitzen

Für eine SaaS ist der Lastspitzen-Test der realistischste Test. Ein Product Hunt Launch, ein Treffer auf der Startseite von Hacker News oder der Versand einer Marketing-E-Mail können den Traffic in einem Fünf-Minuten-Fenster verzehnfachen. Bei einer Cloudflare D1-Datenbank beeinträchtigt diese Spitze nicht die Lesezugriffe – Replicas fangen das ab –, aber sie kann die Obergrenze des Write-Locks sprengen, sollten diese verzehnfachten Anfragen auch Mutationen umfassen.

Hier sind die ehrlichen Zahlen aus dem Abrechnungs-Vorfall:

  • Vor der Spitze: ~50 Mutationen/s, p99-Schreiblatenz 120 ms.
  • Versand der Marketing-E-Mail um 09:00 Uhr: Traffic stieg innerhalb von 8 Minuten auf ~250 Mutationen/s.
  • Anhaltende Spitze (09:08 bis 09:54 Uhr): Die p99-Schreiblatenz stieg von 120 ms auf 8,2 s, die Timeouts überstiegen 30 % und Stripe-Webhooks begannen sich zurückzuhalten (Backoff).
  • Entschärfung um 09:54 Uhr: /api/billing/usage/record wurde in einen Cloudflare Queue Producer verschoben; der p99 sank innerhalb von 15 Minuten wieder auf 180 ms.

Die Lektion ist, dass diese D1-Obergrenze nicht nur ein simpler Stichpunkt im Marketing ist – es ist ein operationeller Fakt. Für SaaS-Apps, die Lastspitzen auf einem Schreib-Pfad erwarten können, ist das von Queues gestützte Muster nicht bloß optional.

Wo Cloudflare D1 Performance glänzt

Sub-Millisekunden-Lesezugriffe von regionalen Replicas

Für SaaS-Dashboards, Einstellungsseiten und Projektlisten – die überwiegend Lesezugriffe auf eine kleine Menge von Entitäten durchführen – sind die regionalen Replicas von D1 das richtige Primitiv. Ein Lesezugriff von einem Replica innerhalb desselben Kontinents antwortet in meinen Traces in unter 2 ms. Das ist schnell genug, damit die Datenbank in absolut keinem meiner bisherigen Request-Pfade zum Flaschenhals wird.

Null Kosten für Connection-Pools

Cloudflare D1 hat keinen Connection-Pool. Jeder Request erhält ein Binding; das Binding ist aus Sicht des Requests zustandslos. Für eine Workers-native SaaS beseitigt das eine ganze Klasse an betrieblicher Komplexität – kein pgbouncer, kein Tuning von max-connections, kein „Uns sind die Postgres-Verbindungen beim Launch ausgegangen.“ Diese Reduzierung der Angriffs- bzw. Problemfläche ist für Solo-Entwickler, die eine SaaS launchen, extrem bedeutend.

Vorhersehbare SQLite Query-Pläne

Der Query-Planner von SQLite ist gut dokumentiert und konservativ. Laut der SQLite Query Planner-Dokumentation agiert der Planner über verschiedene Versionen hinweg konsistent. Das bedeutet: Der EXPLAIN-Output, den Sie in der Entwicklung sehen, ist exakt jener Plan, der auch in der Produktion abläuft. Für SaaS-Apps, bei denen die Abfrage-Performance berechenbar sein muss, ist dieser Determinismus weitaus wertvoller als ein clevererer Planner, der unangenehme Überraschungen birgt.

Wo Cloudflare D1 Performance einbricht

Die Write-Lock-Obergrenze

Das mit Abstand schärfste Performance-Limit in Cloudflare D1 ist der Write-Lock auf Datenbankebene. Jedes INSERT, UPDATE oder DELETE erzwingt den Lock; parallele Schreibvorgänge werden serialisiert. Die genaue Obergrenze variiert je nach Datenbankgröße und Abfrageform, aber die Struktur bleibt identisch: Es gibt einen extrem steilen Abfall, sobald ein paar hundert Mutationen pro Sekunde überschritten werden. Bei schreiblastigen SaaS-Lösungen – Abrechnungen, Metriken-Ingest, Audit-Logs – besteht die richtige Architektur darin, den benutzerseitigen Pfad vom tatsächlichen Schreibvorgang der Datenbank zu entkoppeln, was fast immer den Einsatz einer Cloudflare Queue voraussetzt.

Inkonsistenz beim Read-after-Write zwischen Regionen

Das zweitschärfste Limit ist die Replikationslücke zwischen Primary und Replicas. Ein Benutzer in WEU erstellt ein Projekt, navigiert zur Projektseite und bekommt ein 404. Grund: Dieser Lesezugriff wurde von einem WEU-Replica bedient, das im Vergleich zum Primary in ENAM noch nicht aktuell ist. Der Fix dafür ist das Setzen des Blocks resolve: "primary" im Read-Binding, allerdings kostet das den regionalen Geschwindigkeitsvorteil. Nutzen Sie es daher nur für den kritischen Pfad unmittelbar nach dem Schreiben (Post-Write).

Hot-Partition-Muster in Multi-Tenant SaaS

Das dritte Limit sind mandantenspezifische „Hot Partitions“. In einer SaaS, in der ein einzelner Mandant (Tenant) einen unverhältnismäßigen Anteil an Schreibzugriffen erzeugt – z. B. ein Whale-Kunde in einem Usage-Billing-Modell oder ein B2B-Account, der tausende Anfragen pro Minute erzeugt –, kann sich auf dem Schreibpfad dieses Mandanten der Datenbank-Lock aufstauen, während die übrigen Mandanten weiterhin normale Latenzen erleben. Die Lösung hierfür ist ein mandantenbezogenes Rate-Limit direkt vor dem Worker-Handler gepaart mit einem von Queues geförderten Schreibpfad, damit der benutzerseitige Request umgehend antworten kann.

Bewährte Performance-Muster für SaaS-Standards mit Cloudflare D1

Composite Indexes mit führender tenant_id

In absolut jeder meiner Cloudflare D1-Apps steht die Spalte tenant_id an der ersten Stelle in jedem Composite Index für Domänen-Tabellen. Der Grund: In einer Multi-Tenant SaaS wird ohnehin jede Abfrage implizit nach Mandanten gefiltert, und ein Index, der tenant_id als erstes führt, kann diese Abfragen bedienen, ohne die Zeilen der anderen Mandanten zu berühren. Eine Abfrage gegen (tenant_id, created_at) ist schlicht ein einzelner Index-Range-Scan; ein Suchlauf primär gegen (created_at) ist hingegen ein Full Scan, der die Mandanten erst im Anschluss filtern würde.

sql
-- Pattern: jeder Domänen-Index beginnt mit tenant_id
CREATE TABLE projects (
  id           TEXT PRIMARY KEY,
  tenant_id    TEXT NOT NULL,
  name         TEXT NOT NULL,
  created_at   INTEGER NOT NULL DEFAULT (unixepoch())
);

CREATE INDEX idx_projects_tenant_created
  ON projects(tenant_id, created_at DESC);

CREATE INDEX idx_projects_tenant_active
  ON projects(tenant_id, archived_at)
  WHERE archived_at IS NULL;

Der Partial Index auf archived_at IS NULL sorgt dafür, dass dieser Index auch bei wachsender Tabelle stets klein bleibt. Laut der SQLite Query Planner-Dokumentation verwendet der Planer exakt diesen partiellen Index für die dominierende Abfrage nach „aktiven Projekten“ und ignoriert ihn bei Archiv-Scans.

db.batch() für transaktionale Multi-Row-Schreibvorgänge

Jede logische Transaktion, welche mehrere Tabellen tangiert – Dokumentierung von Rechnungszahlungen oder das Erfassen eines Stripe-Webhooks – sollte in einem einzelnen db.batch() zusammengefasst und nicht in drei separaten, sequenziellen await env.DB.prepare(...).run()-Aufrufen ausgeführt werden. Innerhalb dieser Batch-Schleife werden entweder sämtliche Statements verbucht, oder eben gar keines, und diese Schleife verursacht nur einen einzigen statt drei Netzwerkaufrufen. Der offizielle D1 Best Practices-Guide erwähnt dieses synchrone Muster explizit als Anti-Pattern.

typescript
// src/server/billing.ts
export async function recordInvoicePayment(invoiceId: string, amount: number) {
  return await env.DB.batch([
    env.DB.prepare(
      'UPDATE invoices SET status = ?, paid_at = unixepoch() WHERE id = ?'
    ).bind('paid', invoiceId),
    env.DB.prepare(
      'INSERT INTO payments (id, invoice_id, amount, created_at)
       VALUES (?, ?, ?, unixepoch())'
    ).bind(crypto.randomUUID(), invoiceId, amount),
    env.DB.prepare(
      'UPDATE tenants SET balance_cents = balance_cents - ?
       WHERE id = (SELECT tenant_id FROM invoices WHERE id = ?)'
    ).bind(amount, invoiceId),
  ])
}

Cloudflare Queues für den Write Fan-out

Für sämtliche Write-Workloads, die nicht zwingend synchron abgewickelt werden müssen – z. B. Webhook-Auslieferungen oder das Befüllen von Audit-Logs – sind Cloudflare Queues das unschlagbare Primitiv der Wahl. Der Producer besteht aus einem einfachen env.WRITE_QUEUE.send(...)-Call, der unmittelbar und verzögerungsfrei „zurückkehrt“; der Consumer wiederum verpackt bis zu 100 Nachrichten in einem Queue-Stapel und überführt sie per db.batch() in einem Schritt. Dieses Muster sorgt ganz pragmatisch dafür, dass Audit-Logs nicht mehr asynchron mit den benutzerseitigen Schreibprozessen in Konflikt geraten, wie ausführlich im D1 Deep-Dive-Artikel dokumentiert.

Wenn Cloudflare D1 Performance nicht ausreicht

Es gibt Ausprägungen von SaaS-Anwendungen, für die das Performance-Profil von Cloudflare D1 völlig fehl am Platz ist. Ist Ihre App auf Datenbankebene grundsätzlich hochgradig schreiblastig unterwegs – z. B. bei High-Frequency-Trading-Ledgern, groß dimensionierten Telemetrie-Ingests oder Echtzeit-Analytics auf großen Event-Streams –, wird das Limit des Write-Locks gnadenlos zuschlagen. In diesen Fällen ist der korrekte architektonische Hebel ein Datenbank-Service, der primär auf schieren Schreibdurchsatz optimiert wurde: ein sharded Postgres (wie Neon oder Supabase), ein skalierter Streaming Store (ClickHouse, Tinybird) oder auch das Pattern eines queue-gestützten Durable Objects je einzelner Entität.


Wenn Sie Cloudflare D1 für Ihre SaaS evaluieren, schauen Sie sich die TanStack Ship Features-Seite, unseren D1 Production Guide sowie den Cloudflare D1 vs. Alternativen-Vergleich an. Weitere Insights zum Vorfall, der mich den Wert dieses Queue-gestützten Patterns gelehrt hat, finden Sie im D1 Write-Lock Postmortem. Tiefer in das Query-Tuning steigen Sie mit dem D1 Optimization Writeup ein.