Database scaling lessons from Cloudflare D1 design
Database scaling is the central lesson in Cloudflare’s description of D1, its SQL database service built around many small databases rather than one large shared database. The design separates the control plane, meaning the management side that creates, deletes and tracks databases, from the hot path, meaning the live route a user query takes when an application is waiting for an answer.
When a user creates a D1 database, Cloudflare says several steps happen before queries can run. The system checks whether the account is allowed to create another database under its free tier or account limits. It then generates a unique D1 database UUID, which is a unique identifier, creates a unique Durable Object ID using newUniqueId(), initializes the underlying Durable Object, stores one metadata row in Postgres, and writes a mapping from the database ID to the Durable Object ID in Workers KV. Durable Objects are Cloudflare’s stateful compute objects, used here as the per-database execution point. Workers KV is a globally distributed key-value store used for routing data.
The placement of those steps matters. Cloudflare says the initial Edge API Worker runs close to the incoming request, but creation requests that make multiple round trips to Postgres are forwarded to data centers near the Postgres cluster. The reason is latency: a round trip from far away can take hundreds of milliseconds, while a nearby one can take only a few milliseconds. By default, a D1 database is created near the request location, although a user can provide a location hint or jurisdiction, using the same options supported by Durable Objects. To keep the database near the user while still reducing Postgres latency, Cloudflare forwards execution internally near Postgres, then invokes the Durable Object from a data center close to the original request location.
Cloudflare treats the Postgres write as the creation checkpoint. After the Durable Object is initialized, the database metadata is written to the central Postgres primary through Hyperdrive, Cloudflare’s database connectivity layer. If Durable Object initialization or the Postgres write fails, the worker retries with different identifiers. Once the row is in Postgres, the database is considered created even if the final response does not reach the user. A final confirmation call to the Durable Object runs in waitUntil(), so it can fail without blocking creation. That call tells the Durable Object the database is active and lets it update Workers KV so later queries can find routing metadata quickly.
A tempting alternative would be to put the whole creation flow in one Postgres transaction, which is a database operation that either fully completes or fully rolls back. Cloudflare says that would not scale because database connections would be held open while a Durable Object is initialized, potentially on the other side of the world. The broader lesson is that a central database should not be forced to wait on long-distance external work when the platform needs to create large numbers of resources.
D1 also uses heartbeats to repair state after partial failures. Each D1 Durable Object periodically contacts the control plane to fetch the latest database state, active or deleted, and account limits. Cloudflare says the interval is 30 seconds, with per-database jitter, and that it has controls to change the interval per database or account or disable heartbeats entirely. Because heartbeats can tolerate slightly stale data, they can be served from Hyperdrive caches or Postgres read replicas, which are read-only copies of the main database.
The retry design explains why new identifiers are used after a failed creation attempt. If Cloudflare reused the same database UUID with a fresh Durable Object ID, more than one Durable Object could believe it owned the same database. In Cloudflare’s example, Durable Object A may initialize successfully but fail before the Postgres write. The retry initializes Durable Object B and successfully writes B to Postgres. Users can only query B because Postgres is the source of truth. Within 30 seconds, A heartbeats, learns its database does not exist, and marks itself deleted. The same self-correcting pattern also applies to deletion, and heartbeats eventually repair Workers KV if a best-effort update fails.
For live SQL queries, Cloudflare’s main rule is to avoid Postgres or any other centralized single-region component. A query made over the HTTP API first reaches the Edge API Worker, which handles authentication, authorization, rate limiting, request validation and entitlement checks. It then tries to load the database mapping from Workers KV. If that item is missing, the system falls back to Hyperdrive against Postgres replicas. Hyperdrive first checks a cache in the caller’s location, then a cache near the Postgres cluster, and only then queries the replicas.
After the mapping is found, the Edge API Worker calls the Eyeball Worker through Worker Service Bindings, a mechanism for one Worker to call another. The Eyeball Worker validates the request and forwards it to the D1 Durable Object, which executes the SQL query and returns the result. Queries from a user Worker with a D1 Binding do not need to resolve the D1 UUID mapping, because the binding already carries the needed information, so they go directly to the Eyeball Worker and Durable Object.
Cloudflare also explains why D1 uses Workers KV even though Hyperdrive has caching. Workers KV is used as a durable datastore, not just a temporary cache. If an item leaves KV’s edge cache, the read comes from KV’s own storage rather than Postgres. Cloudflare warns that in a catastrophic cache purge scenario, a million databases fetching routing information from Postgres could create metastable failures, where load caused by recovery prevents recovery from succeeding.
Usage limits are handled asynchronously. Cloudflare says D1 has, at the time of writing, a 5 GB per-account storage limit on the Free plan and 1 TB on Workers Paid, with the paid limit raisable on request. Each Durable Object publishes storage usage updates to a Kafka-like queue, which is a system for buffering events so consumers can process them later. Queue consumers run in Cloudflare’s central data centers, while Durable Objects are spread across the edge. This smooths and batches writes before they reach Postgres.
For enforcement, D1 needs account-wide storage totals, not only per-database sizes. Cloudflare says running SELECT sum(storage) across every database for an account would scale linearly and would not work for accounts with millions of D1 databases. Instead, D1 writes per-database sizes to database_stats, uses a trigger to write deltas into account_database_stats_changes, and runs a scheduled job every second to aggregate those deltas into account_database_stats. The query uses DELETE ...