· cloudflare / d1 / database
Cloudflare D1 in 2026: is it production-ready?
D1 cleared production-ready for read-heavy Workers apps in 2026. Two limits remain: ~10 writes/sec per database and always-on foreign key enforcement.
By Ethan
1,910 words · 10 min read
D1 is a legitimate primary database for read-heavy Cloudflare Workers applications in 2026. The past two years of improvements are real: a 40–60% latency reduction in January 2025, global read replication in public beta since April 2025, and account storage now at 1 TB. If your application is write-heavy, relies on multi-writer patterns, or depends on ORM migration tooling that expects a standard SQLite environment, D1 will frustrate you before it serves you — evaluate Postgres instead.
Who this is for
TypeScript developers shipping on Cloudflare Workers who want zero-ops data persistence without leaving the Workers ecosystem. This review covers the TypeScript Worker path only — no REST API, no Python, no edge cases outside the stack D1 is designed for. If you’re not already on Workers, D1 offers nothing a conventional managed database doesn’t.
What we tested
This review draws from Cloudflare’s primary documentation: D1 platform limits, release notes, changelog, SQL statements reference, and foreign keys docs — cross-referenced with the Workers SDK changelog and GitHub issues from cloudflare/workers-sdk, cloudflare/workerd, and drizzle-team/drizzle-orm. Pricing sourced from cloudflare.com, May 2026.
What D1 is and where it fits
D1 is a managed SQLite database that runs inside Cloudflare’s network, co-located with Workers. Each D1 database is a single SQLite file with one primary that accepts all writes. Reads are served from asynchronously replicated copies distributed to Cloudflare PoPs globally.
This is a different mental model from Postgres-based managed databases. Think of it as “your SQLite file, globally available, zero-ops” — not a horizontally scalable relational engine. The write ceiling is structural, not a tuning parameter.
The core pitch is co-location: your Worker and your D1 database live in the same network, often the same PoP. For read-heavy workloads already running at the edge, this eliminates the round-trip to a separate database region. For write-heavy workloads, the architecture changes nothing about where the bottleneck lives. For how D1 stacks up against other edge-native databases on latency and consistency, see edge database tradeoffs: when latency is a lie.
What has changed since GA (April 2024 → May 2026)
D1 shipped to general availability in April 2024 with global read replication described as “under active development.” Two years later, the product has materially improved across latency, storage, and local development parity.
January 2025: 40–60% latency reduction
On January 7, 2025, Cloudflare eliminated redundant TCP round trips in the D1 Worker API path, delivering up to 60% improvement across p50, p90, and p95 percentiles. This was the single biggest operational fix since launch — the pre-fix overhead had generated consistent complaints in community threads and HN discussions.
Source: Cloudflare changelog, 2025-01-07.
April 2025: global read replication (public beta)
A full year after GA, read replication entered public beta. The feature distributes read queries to PoPs near the requester. Without it, all reads routed to the primary region — which partially negated D1’s co-location advantage for globally distributed users.
The Sessions API shipped alongside read replication. It uses bookmark-based Lamport timestamps to guarantee sequential consistency within a session: after any write, all subsequent reads in that session are guaranteed to see the write.
Without the Sessions API, reads from replicas can return stale data silently. Applications that enable read replication without configuring the Sessions API are trading consistency for latency they may not notice until a user reports seeing stale state.
The current D1 docs no longer carry a “beta” warning on read replication, but no explicit GA announcement appeared in the sources reviewed. Verify against the current docs before treating it as production-stable in regulated or high-consistency workloads.
Source: Read Replication Blog, Read Replication Best Practices.
July 2025: account storage 4× increase
Account-level storage moved from 250 GB to 1 TB on July 1, 2025. Per-database limits are unchanged: 500 MB on Free, 10 GB on paid. The limit increase primarily benefits teams running many D1 databases — paid accounts support up to 50,000 databases.
Source: D1 Release Notes.
April 2026: local/remote foreign key parity fixed
Until April 2026, Wrangler’s local D1 emulator had foreign keys disabled by default while remote D1 had them always enabled. The gap meant FK violations that never surfaced in local development could break production deployments silently. GitHub issue workers-sdk#5092 closed as completed in April 2026.
If you ran a D1 project in local development before this fix, audit your FK constraints against a remote database before your next schema migration.
Limits and gotchas
Storage and scale
| Plan | Per-DB limit | Account total | Max databases |
|---|---|---|---|
| Free | 500 MB | 5 GB | 10 |
| Paid (Workers $5/mo) | 10 GB | 1 TB | 50,000 |
Source: D1 Platform Limits.
The free tier covers most personal projects. The paid tier’s 10 GB per-database cap handles early-stage apps comfortably, but 10 GB goes fast once you add blob metadata, audit logs, or full-text search indexes.
The write ceiling
“D1 databases are inherently single-threaded, and process queries one at a time.” — D1 Platform Limits
At 100ms average query time, this means roughly 10 writes per second per database. All writes route to one primary regardless of read replication. Cloudflare’s own documentation recommends Durable Objects for write-intensive workloads.
This is not a gap Cloudflare can close with a configuration flag — it’s the SQLite file model. If your application handles comment feeds, real-time game state, high-frequency event ingestion, or any queue-backed write workload, D1 will become your bottleneck before you reach meaningful user scale.
Consistency model
Read replication is opt-in. The Sessions API that provides sequential consistency is also opt-in. The default with read replication enabled is eventual consistency — stale reads with no warning.
Configure the Sessions API explicitly:
// Always create a session for write-then-read flows
const session = env.DB.withSession('first-primary');
await session.prepare('INSERT INTO events (user_id, type) VALUES (?, ?)').bind(userId, 'login').run();
const user = await session.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
// This read is guaranteed to see the write above
Without the session, the SELECT could be served from a replica that hasn’t yet seen the INSERT.
Foreign key enforcement: always-on, no escape hatch
D1 enforces foreign keys (PRAGMA foreign_keys = ON) on every implicit transaction. PRAGMA foreign_keys = OFF is silently ignored — D1 does not acknowledge the PRAGMA; it does not return an error.
This breaks Drizzle ORM’s D1 migration path. Drizzle generates PRAGMA foreign_keys=OFF at the top of migrations to allow destructive schema changes that temporarily violate FK constraints. D1 ignores it. If a migration would violate a FK constraint mid-execution, D1 throws an error and leaves the schema in a partial state.
Active GitHub issues: drizzle-orm#4212, drizzle-orm#4089.
The workaround: prepend PRAGMA defer_foreign_keys = ON to your migration. This delays FK enforcement to end-of-transaction rather than mid-statement. It is not a full equivalent of disabling FKs, but it unblocks most Drizzle migration patterns until the ORM ships a D1-aware migration mode.
-- Add this to the top of migrations that touch FK-constrained tables
PRAGMA defer_foreign_keys = ON;
-- Your migration SQL follows
ALTER TABLE orders DROP COLUMN legacy_id;
Source: D1 Foreign Keys.
SQL feature gaps
D1 runs SQLite’s query engine with notable restrictions:
- No explicit transactions —
BEGIN,COMMIT, andROLLBACKare not supported at the SQL level; use D1’s batch API for multi-statement atomicity - Limited PRAGMAs — only a subset is supported; PRAGMA statements apply per-transaction, not per-session
- Three extensions only — FTS5, JSON, and math; third-party SQLite extensions are unavailable
Source: D1 SQL Statements.
Real-world verdict
| Use case | Fit | Reason |
|---|---|---|
| Solo project / personal app | ✅ Strong | Free tier covers most personal apps. Zero-ops, co-located with Workers, no cold-start overhead on reads. |
| Small startup / read-heavy SaaS | ✅ Good with caveats | 10 GB handles early stage. Read replication + Sessions API delivers fast, consistent global reads. Drizzle users need the defer_foreign_keys workaround until the ORM adapts. |
| Write-heavy / multiplayer / real-time | ❌ Wrong tool | Single-threaded primary saturates around 10 writes/second. Cloudflare itself recommends Durable Objects for this pattern. |
| Enterprise / compliance workloads | ⚠️ Evaluate carefully | No point-in-time recovery in documented feature set. Write availability routes through a single primary with no multi-region failover. |
Migration story: when you outgrow D1
Three paths come up in practitioner accounts when teams hit D1’s limits.
Option A: Hyperdrive + Postgres
Hyperdrive is Cloudflare’s connection-pooling proxy for external Postgres databases. It makes Neon, Supabase, or Railway Postgres performant from Workers by pooling connections and minimizing round-trip overhead. You stay on the Workers stack and swap the database layer. The lift is schema migration — no novel architecture required. A practitioner migration from D1 to Hyperdrive is documented at mats.coffee/blog/d1-to-hyperdrive. For choosing a managed Postgres provider to pair with it, see best Postgres hosts for early-stage SaaS.
Option B: Turso
Turso is a SQLite-compatible edge database built on libSQL — a SQLite fork that adds multi-writer support. The schema migration from D1 to Turso is the most straightforward of the three options: both use SQLite syntax, and the data model translates directly. Turso supports proper multi-writer topologies and scales past D1’s single-primary write ceiling. A large-scale D1-to-Turso migration is documented at lukasnotes.dk/migrating-large-d1-to-turso. For a direct comparison of the two, see Turso vs Cloudflare D1.
Option C: D1 sharding
Paid accounts can run up to 50,000 D1 databases. For read-heavy workloads that outgrow the 10 GB per-database cap, splitting into multiple databases and routing at the Worker layer is a viable pattern. Cloudflare handles storage transparently; you handle the routing key. This addresses storage limits but does nothing for the write ceiling — sharding by tenant doesn’t add write throughput per tenant.
Bottom line
D1 is the right choice for read-heavy, TypeScript-on-Workers applications in 2026 where you want zero operations overhead. The January 2025 latency improvements made it genuinely fast; the 1 TB account storage limit removed the scaling anxiety for most use cases; and read replication with the Sessions API gives you edge-consistent reads when you configure it correctly.
The write ceiling and the always-on FK enforcement are the real gatekeepers — not marketing language about “beta status.” If you’re building anything that writes frequently, or if your team is deep into a Drizzle workflow and can’t absorb migration friction, D1 will cost you time.
For solo developers and read-heavy startups already on Workers: D1 is the right starting point. Ship on it, instrument your write patterns, and migrate to Hyperdrive + Postgres or Turso when the ceiling appears.