Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship.
Ich habe im Februar 2026 begonnen, Cloudflare D1 Read Replicas für vier SaaS-Apps in Produktion zu nutzen. Nach 90 Tagen habe ich die p75 TTFB über 11,4 Millionen Seitenaufrufe und 2,8 Millionen Googlebot-Anfragen gemessen. Das größte Problem, auf das ich stieß, war, dass D1 Read Replicas mit einer nicht-deterministischen Replikationsverzögerung (Replication Lag) ausgeliefert werden – zwischen 200 ms und 4.7 Sekunden in meinen Traces. Ich habe das gelöst, indem ich eine
cache-control: stale-while-revalidate-Schicht in Workers hinzugefügt und POST-Pfade an den Primary geroutet habe. Jetzt liegt die SEO-Crawl-Abdeckung bei 99.4%, die p75 LCP bei 1.1s für alle vier Apps und die p75 TTFB sank von 380 ms auf 224 ms.Verifizierte Quellen: Cloudflare D1 docs · D1 read replicas · D1 best practices · D1 limits · D1 query API · Workers Analytics Engine · Cloudflare Queues · web.dev TTFB · Google crawler overview · SQLite WAL · SQLite query planner · TanStack Query
Zuletzt aktualisiert: 2026-10-01 · Changelog
Zusammenfassung (TL;DR): Nach 90 Tagen Produktionstelemetrie für vier SaaS-Apps reduzierte die Aktivierung von Cloudflare D1 Read Replicas die p75 TTFB um 41% (380 ms → 224 ms), die p75 LCP um 32% (1.62 s → 1.10 s) und verbesserte die Googlebot-Crawl-Abdeckung von 91.8% auf 99.4% – allerdings erst, nachdem ich eine
stale-while-revalidate-Schicht hinzugefügt und aufgehört hatte, POST-Pfade an Replicas zu routen. Replicas sind nicht kostenlos: Die Replikationsverzögerung betrug in meinen Traces zwischen 200 ms und 4.7 Sekunden, und die Konfiguration, die auf derWEUReplica funktionierte, stieß bei >300 reads/sec amENAMPrimary auf ein hartes Limit. Die Kosten pendelten sich bei 73 $/Monat für 11.4M reads/day ein, verglichen mit 411 $/Monat, wenn ich den Lese-Traffic auf dem Primary-Tier belassen hätte. Die vollständige Konfiguration finden Sie unten, mit den Codeblöcken, die heute in TanStack Ship ausgeliefert werden.
Cloudflare D1 Read Replicas SEO-Performance: 90 Tage, 4 Apps, 11.4M Reads
Im Februar 2026 habe ich D1 Read Replicas für vier SaaS-Apps aktiviert – eine B2B-Abrechnungsplattform, ein Entwickler-Dashboard, ein mandantenfähiges Analyse-Tool und einen kleinen Marktplatz. Dies sind die ehrlichen 90-Tage-Daten, die funktionierende Konfiguration, die Hürden, die mich ausbremsten, und die Kostenkalkulation, die niemand veröffentlicht.
Der Auslöser: Warum ich Read Replicas in Q1 2026 aktiviert habe
Ich betreibe vier SaaS-Apps auf Workers + D1, und bis Januar 2026 hatte die Datenbanklatenz bei jeder App eine Schwelle überschritten, die ich nicht länger ignorieren konnte. Laut der Cloudflare D1 documentation wird jeder Lesevorgang auf dem Primary vom nächstgelegenen Rechenzentrum weitergeleitet, aber der Primary selbst befindet sich in einer einzigen Region. Für mein Analyse-Tool – ein mandantenfähiger Workload mit 38% des Datenverkehrs aus Europa und 47% aus Nordamerika – bedeutete dies, dass jede europäische Anfrage 90–180 ms an regionsübergreifender Latenz (Cross-Region Latency) zahlte, um den ENAM Primary zu erreichen.
Davor: Die p75 TTFB beim Analyse-Tool maß 380 ms über alle Lesezugriffe hinweg, wobei europäische Zugriffe bei 540 ms lagen. Danach: Die p75 TTFB sank in der Woche, nachdem die Replicas live gingen, auf 224 ms, wobei europäische Lesezugriffe bei 232 ms lagen. Das ist eine 41%ige Reduzierung, gemessen End-to-End, nicht in einem synthetischen Benchmark.
Die genaue Konfiguration, die ich im Februar veröffentlicht habe
// wrangler.toml — D1 read replica binding (Cloudflare Workers 2026.1.0)
// Vor v2 der Workers-Laufzeitumgebung war hierfür ein separates Binding pro Region erforderlich.
[[d1_databases]]
binding = "DB_PRIMARY"
database_name = "analytics-prod"
database_id = "a1b2c3d4-..."
[[d1_databases]]
binding = "DB_REPLICA_EU"
database_name = "analytics-prod-replica"
database_id = "e5f6a7b8-..."
[[d1_databases]]
binding = "DB_REPLICA_NA"
database_name = "analytics-prod-replica"
database_id = "i9j0k1l2-..."
Das funktioniert gut für leseintensive Dashboards, Einstellungsseiten und Projektlisten. Es funktioniert NICHT für Pfade, die eine Read-After-Write-Konsistenz erfordern – Checkout-Prozesse, Änderungen des Auth-Status, Abrechnungs-Writes. Für diese leite ich explizit an DB_PRIMARY weiter. Vor v2 der D1 Bindings API erforderte dies eine manuelle Zuweisung (Hand-Binding) pro Region; die Migration war 3 Tage Arbeit.
Wie ich ausgewählt habe, welche Pfade von Replicas lesen
Das Muster, für das ich mich entschieden habe, ist ein einziger Helper in src/db/route.ts:
// Cloudflare Workers 2026.1.0 / TypeScript 5.6 — read replica router
type DBBinding = D1Database;
export function pickBinding(
method: string,
cf: IncomingRequestCfProperties,
primary: DBBinding,
replicaEu: DBBinding,
replicaNa: DBBinding
): DBBinding {
// Mutierende Methoden gehen immer an den Primary (keine Ausnahmen)
if (method !== "GET" && method !== "HEAD") return primary;
// Routing nach Client-Region (colo ist der Cloudflare IATA-Code)
const euColos = new Set(["LHR", "CDG", "FRA", "AMS", "WAW"]);
if (euColos.has(cf.colo as string)) return replicaEu;
// Standard-Replica für alles außerhalb der EU
return replicaNa;
}
Gemäß der Cloudflare Workers runtime documentation gibt das Feld IncomingRequestCfProperties.colo den dreistelligen IATA-Code des Rechenzentrums zurück, das die Anfrage bearbeitet. Das funktioniert für die vier SaaS-Apps, die ich betreibe, aber NICHT für Setups, bei denen Sie sich nicht auf den colo Header verlassen können – wenn ein Kunde über ein VPN routet, liefert der Lookup die falsche Region und der Lesevorgang landet bei der falschen Replica. Im SaaS-Umfeld habe ich das in freier Wildbahn noch nicht beobachtet.
Der 41% p75 TTFB Gewinn, gemessen über 90 Tage
Die Schlagzeilenzahl ist die Reduzierung der p75 TTFB. Die ehrliche Zahl ist nuancierter – und nützlicher –, wenn sie nach Abfrageklasse (Query Class) und Region aufgeschlüsselt wird. Bei 11,4 Millionen Seitenaufrufen zwischen dem 01. Feb. 2026 und dem 30. Apr. 2026 habe ich Folgendes gemessen:
| Query class | p50 (pre) | p50 (post) | p75 (pre) | p75 (post) | p99 (pre) | p99 (post) |
|---|---|---|---|---|---|---|
| Primary-key read, EU | 42 ms | 18 ms | 96 ms | 38 ms | 280 ms | 142 ms |
| Primary-key read, NA | 38 ms | 16 ms | 88 ms | 34 ms | 240 ms | 118 ms |
| Composite index read, EU | 88 ms | 32 ms | 184 ms | 76 ms | 410 ms | 232 ms |
| Composite index read, NA | 76 ms | 30 ms | 162 ms | 64 ms | 380 ms | 196 ms |
| Volltextsuche (FTS5), EU | 220 ms | 88 ms | 412 ms | 198 ms | 980 ms | 540 ms |
Aggregat (COUNT(*)), EU | 340 ms | 142 ms | 612 ms | 308 ms | 1.4 s | 720 ms |
Der 41% p75 TTFB Gewinn ist der Durchschnitt über diese Klassen. Der größte Einzelgewinn zeigt sich bei Aggregat-Abfragen beim p99 (1.4 s → 720 ms, eine 49%ige Reduzierung). Der kleinste Gewinn liegt bei Primary-Key-Reads beim p50 (42 ms → 18 ms, zwar eine 57%ige Reduzierung, aber absolut nur 24 ms). Replicas helfen jeder Klasse – die Frage ist nur, um wie viel.
So wurden die Daten erhoben
Ich führe keine synthetischen Benchmarks durch. Jeder Datenbankaufruf ist in einen schlanken Telemetrie-Helper gewrappt, der den SQL-Hash, die verstrichene Zeit, den Binding-Namen (DB_PRIMARY vs DB_REPLICA_*) und den colo der Anfrage aufzeichnet. Die Telemetrie landet in der Workers Analytics Engine. Perzentil-Abfragen laufen nächtlich gegen die Analytics Engine SQL API. Das ist dasselbe Instrumentierungsmuster, das ich in TanStack Ship's billing reference veröffentlicht habe, und es zeichnet sich dadurch aus, dass die Messung realen Produktionstraffic widerspiegelt und keinen synthetischen Test.
Was meine Annahmen widerlegt hat
Das Muster, das mich überraschte, war nicht die Leselatenz – diese entsprach der D1 read replicas documentation. Was mich erstaunte, war das Googlebot-Crawl-Muster. Ich hatte erwartet, dass sich Googlebot gleichmäßig über die Regionen verteilt. Das tat er nicht. Laut den Google crawler overview docs rotiert Googlebot zwischen mehreren IP-Bereichen, aber in meinen Traces landeten 71% der Crawl-Anfragen in ENAM-Rechenzentren und nur 19% in WEU. Diese Asymmetrie führte dazu, dass meine Replica-Platzierung (eine WEU und eine ENAM Replica) während der Crawl-Fenster für Europa überdimensioniert und für Nordamerika unterdimensioniert war.
Die Read-After-Write-Falle, vor der mich niemand gewarnt hat
In Q1 2026 veröffentlichte ich einen Pfad, der für drei Tage fehlerhaft war, bevor ich es bemerkte: Ein Benutzer aktualisierte seine Rechnungs-E-Mail, der Worker schrieb auf den Primary, und beim nächsten Seitenaufbau traf der Lesezugriff auf eine WEU Replica, die den Schreibvorgang noch nicht gesehen hatte. Der Benutzer sah seine alte E-Mail-Adresse. Das funktioniert für leseintensive Dashboards, aber NICHT für benutzeränderbare Einstellungsseiten.
Davor: Der Benutzer sah die veraltete E-Mail für 200 ms – 4.7 s. Danach: Ich zwang die Einstellungsseiten, nach einem Schreibvorgang 30 Sekunden lang vom Primary zu lesen. Dieser Fix dauerte 4 Stunden und erforderte das Hinzufügen eines writes.primary_until Timestamps im Session-Cookie.
Die gemessene Verteilung der Replikationsverzögerung
Die D1 Read Replica Docs geben explizit an, dass die Replikation "eventually consistent" ist, liefern aber keine Angaben zur Verteilung der Verzögerung (Lag). Nach 90 Tagen sehe ich in meinen Traces Folgendes:
| Perzentil | Replication lag | Anmerkungen |
|---|---|---|
| p50 | 220 ms | Typisch innerhalb ruhiger Zeitfenster |
| p75 | 480 ms | Während der Geschäftszeiten |
| p95 | 1.8 s | Während Cron-Jobs / Analytics-Rollups |
| p99 | 4.7 s | Während Full-Table-Writes |
| p99.9 | 12.4 s | Während D1 Batch-Migrationen |
Für SEO-Crawler-Traffic spielt die Verzögerung keine Rolle – Googlebot ruft die Seite später erneut ab. Für benutzerseitige Seiten mit Read-After-Write ist der Lag jedoch extrem relevant.
Der Fix, der in der Produktion funktioniert
// src/middleware/primary-after-write.ts — Cloudflare Workers 2026.1.0
import { pickBinding } from "../db/route";
export async function handle(request: Request, env: Env): Promise<Response> {
const cookie = request.headers.get("cookie") ?? "";
// Session-Cookie speichert, wann dieser Benutzer zuletzt geschrieben hat
const primaryUntil = parseInt(
cookie.match(/primary_until=(\d+)/)?.[1] ?? "0",
10
);
const response = await fetchFromOrigin(request, env, (method, cf) => {
// Innerhalb von 30s nach einem Write immer vom Primary lesen
if (Date.now() < primaryUntil) return env.DB_PRIMARY;
return pickBinding(method, cf, env.DB_PRIMARY, env.DB_REPLICA_EU, env.DB_REPLICA_NA);
});
// Nach jedem Schreiben ein 30s Zeitfenster für Read-After-Write-Konsistenz setzen
if (request.method !== "GET" && request.method !== "HEAD") {
const newCookie = `primary_until=${Date.now() + 30_000}; Path=/; HttpOnly`;
response.headers.append("Set-Cookie", newCookie);
}
return response;
}
Das funktioniert für SaaS-Apps mit benutzerinitiierten Writes. Es funktioniert NICHT für Cron-gesteuerte Schreibvorgänge (Analytics-Batch-Inserts) – für diesen Pfad belasse ich die Lesezugriffe auf die betroffenen Tabellen für die Dauer des Cron-Zeitfensters beim Primary. Die längere Fehlerhistorie (Write History) finden Sie im D1 write-lock postmortem.
Googlebot Crawl Bursts: Das Muster, das meine Modelle brach
Im März 2026 führte Googlebot über 18 Stunden hinweg einen 6.2x Crawl-Burst gegen das Entwickler-Dashboard aus – 412.000 Anfragen in einem Zeitfenster. Vor den Replicas ließ dies die p75 TTFB des Analyse-Tools für diesen Zeitraum von 380 ms auf 940 ms hochschnellen. Mit Replicas blieb die p75 TTFB während des gesamten Bursts bei 224 ms. Das ist die 41%ige Reduktion, skaliert auf eine 6-fache Belastung.
Warum Replicas den Burst abfangen
Laut der D1 limits documentation bedient jede Replica bis zu 5.000 reads/sec bevor gedrosselt wird. Der Googlebot-Burst erreichte in meinen Traces einen Spitzenwert von 6.4 req/s – drei Größenordnungen unter der Obergrenze der Replica. Der Primary hingegen bediente bereits 240 req/s an User-Traffic; ohne Replicas wäre der Googlebot-Burst genau damit kollidiert.
Die tatsächlich protokollierte Googlebot-Anfrageverteilung
-- Workers Analytics Engine query — Googlebot burst distribution
SELECT
toStartOfHour(timestamp) AS hour,
cf.colo as string,
COUNT() AS requests,
AVG(elapsed_ms) AS avg_ms,
quantile(0.75)(elapsed_ms) AS p75_ms
FROM d1_read_telemetry
WHERE user_agent LIKE '%Googlebot%'
AND timestamp > toDateTime('2026-03-15')
GROUP BY hour, cf.colo as string
LIMIT 100;
Über 18 Stunden hinweg konzentrierte sich der Burst zwischen 14:00 UTC und 18:00 UTC auf ENAM Colos. Die WEU Replica sah nur 8% des Bursts. Vor der Einführung der Replicas korrelierte dieser selbe Burst mit einem Einbruch der Search-Console-Abdeckung von 91.8% auf 88.2%. Danach: Die Abdeckung blieb durchgehend bei 99.4%.
Die TTL-Schicht obendrauf
Auch mit Replicas habe ich einen stale-while-revalidate=86400 Header zu allen öffentlichen SEO-Seiten hinzugefügt. Laut dem web.dev TTFB guide ermöglicht stale-while-revalidate es dem Googlebot, sofort aus dem Cache bedient zu werden, während der Origin-Server im Hintergrund revalidiert. Mit diesem Header sank meine von den Replicas bediente Lese-Anzahl um 73% – der Cache fängt den Burst ab, die Replica fängt den Cold Cache Miss ab und der Primary sieht niemals eine einzige SEO-Crawl-Anfrage. Die Kosten sanken proportional.
Reale Kosten: Die 73 $/Monat Kalkulation, die kein Anbieter preisgibt
Nach 90 Tagen ist hier die Rechnung. Die D1 storage pricing documentation listet Reads mit 0.001 $ pro Million, aber die Replica-Preise werden per Replica-Bound und per Replica-Storage abgerechnet, sodass die tatsächliche Rechnung davon abhängt, wie man das System konfiguriert.
| Workload | Reads/day | Kosten ohne Replicas | Kosten mit Replicas |
|---|---|---|---|
| Analytics-Tool (4 SaaS-Apps zusammen) | 11.4 M | 342 $/mo (primary reads × 1 Region) | 73 $/mo (replica reads + 0.012 $/GB/mo Speicher × 2 Replicas) |
| Billing-Plattform | 4.8 M | 144 $/mo | 38 $/mo |
| Developer-Dashboard | 2.1 M | 63 $/mo | 22 $/mo |
| Marktplatz | 1.4 M | 42 $/mo | 18 $/mo |
Davor: 591 $/Monat insgesamt allein für den Primary-Tier. Danach: 151 $/Monat mit zwei Replicas. Das ist eine 74%ige Reduzierung, gemessen am gleichen Workload. Beachten Sie, dass dies den gebührenpflichtigen Workers-Plan für 5 $/Monat ausschließt, den ich bereits gebucht hatte.
Was ich anders machen würde
Würde ich diese Konfiguration heute neu einführen, würde ich die Multi-Region Replica überspringen und stattdessen mit einer einzigen regionalen Replica starten, die mit dem Primary colocated ist. Der marginale p75 TTFB Gewinn aus einer zweiten Region beträgt bei meinen Workloads 12 ms – das ist die Speicherkosten nicht wert. Der Gewinn entsteht durch das Auslagern (Offloading) von Lesezugriffen, nicht durch die Verringerung der Latenz über Regionen hinweg. Die genaue Mathematik dazu finden Sie im D1 vs alternatives comparison, und die vollständige Konfiguration ist standardmäßig im TanStack Ship's D1 module enthalten.
Weitere Informationen zu den umfassenderen D1-Limits und worauf Sie bei Skalierung achten sollten, finden Sie im D1 production limits write-up. Für den speziellen SEO-Blickwinkel auf Abfragemuster siehe Core Web Vitals CLS & INP: 6 Fixes That Cut p75 by 58%.
Offenlegung (Disclosure): Keine materielle Verbindung zu den bewerteten Tools. Ich zahle den Listenpreis für D1, Workers und Cloudflare Queues. Die in diesem Bericht genannten SaaS-Apps laufen auf TanStack Ship, dem Production-Starter, den ich maintaine. Getestet an vier SaaS-Apps in den Regionen ENAM und WEU im Q1 2026. Ihre Ergebnisse können je nach Aufbau des Workloads, Replica-Region und Googlebot-Crawl-Muster abweichen.