Cloudflare D1 vs PostgreSQL: When to Use It, When Not To

Quick answer: Use D1 when your data shards naturally into small, independent pieces โ€” one database per customer, per user, or per project โ€” and no single piece will exceed 10 GB, which is a hard ceiling that cannot be raised. Do not use D1 when you have one central table that grows without bound, when you need heavy multi-table joins, or when you depend on PostgreSQL extensions like PostGIS or pgvector. In those cases PostgreSQL is still the right answer, and you can connect Workers to it through Hyperdrive without migrating anything.

Engineering teams evaluating Cloudflare Workers hit the same question almost immediately: if compute moves to the edge, where does the database live? Cloudflare answers with D1 โ€” a serverless SQL database attached to your Worker through a binding. No connection pool to size, no port to open, no idle instance to pay for.

The trap is that many people read the phrase serverless SQL database and mentally translate it to managed PostgreSQL. It is not. D1 is SQLite, and its ceilings are hard enough that your architecture has to be designed around them from day one, not patched around them later.

This article covers what those ceilings actually are. Every number comes from Cloudflare official documentation, checked as of July 2026. It also covers the workloads that should stay on Postgres, without any hand-wringing about it.

What D1 actually is

D1 is SQLite, running on Cloudflare infrastructure. Each D1 database is backed by a single Durable Object. That is not a marketing detail โ€” it explains essentially every behaviour of D1, from the storage ceiling to the throughput model to the concurrency characteristics.

The 10 GB ceiling that cannot be raised

A single D1 database holds a maximum of 10 GB on the Workers Paid plan, and 500 MB on the Free plan. Cloudflare documentation is explicit: the 10 GB limit of a D1 database cannot be further increased. Not raised on request, not raised on Enterprise. Ten gigabytes, full stop.

If you read that and thought 10 GB is plenty, do the arithmetic on your largest append-only table. Event logs, audit trails, order line items on a live e-commerce site โ€” tables of that shape pass 10 GB faster than you expect. When a SQLite-backed store fills up, writes fail with a SQLITE_FULL error. Reads and deletes keep working, which is helpful, but you will be doing emergency data pruning at exactly the moment you least want to.

Cloudflare does not treat this as a defect. The documentation states plainly that D1 is designed for horizontal scale out across multiple, smaller (10 GB) databases, such as per-user, per-tenant or per-entity databases. You can create up to 50,000 databases per account on Workers Paid โ€” and that number can be increased on request to millions โ€” with a default total storage cap of 1 TB per account, also increasable. The point is that D1 is not one big database you keep feeding. It is a fabric for building thousands of small ones.

One database means one thread

Because each D1 database is a single Durable Object, it is inherently single-threaded and processes queries one at a time. Cloudflare states the implication directly: your maximum throughput is tied to query duration.

If your average query takes 1 ms, you get roughly 1,000 queries per second. If your average query takes 100 ms, you get roughly 10 queries per second. When a database receives more concurrent requests than it can process, it queues them; when the queue fills, it returns an overloaded error.

This changes what indexes mean. On Postgres, a missing index on a small table is a performance annoyance. On D1, a full table scan does not just cost you money in rows read โ€” it consumes the only thread that database has, and every other request against that shard waits behind it.

The SQL-level limits people forget

Beyond storage, D1 imposes limits you will meet during implementation, not during architecture review. A table can have at most 100 columns. Any single string, BLOB, or table row is capped at 2,000,000 bytes (2 MB). A single SQL statement can be at most 100,000 bytes (100 KB). A query accepts at most 100 bound parameters. Any single SQL query is terminated after 30 seconds.

Two limits matter more than the rest. A Worker can open up to six simultaneous connections to D1 per invocation โ€” the same simultaneous-open-connections ceiling that applies to Workers generally. And a Worker invocation can issue at most 1,000 queries on Workers Paid (50 on Free). If your handler has a loop that runs a query per row, you will hit that faster than you think.

D1 versus PostgreSQL, point by point

This table covers the differences that change a decision, not a feature checklist nobody reads.

Dimension Cloudflare D1 PostgreSQL (RDS / Cloud SQL / self-hosted)
Engine SQLite PostgreSQL
Max size per database 10 GB (Workers Paid) โ€” cannot be increased Bounded by the disk you pay for; multi-terabyte is routine
Databases per account 50,000 (Workers Paid), increasable on request Normally a handful; each one is operational overhead
Scaling model Scale out: many small databases Scale up: bigger instance, plus read replicas
Concurrency Single-threaded per database, one query at a time Multi-process, many concurrent connections
Extensions None. No PostGIS, no pgvector, no TimescaleDB Mature extension ecosystem
Data types SQLite type system (no native enum, array, jsonb, uuid) Full set: jsonb, arrays, ranges, uuid, custom types
Stored procedures / complex triggers No PL/pgSQL Full support
Connecting from Workers Native binding; no connection pool to manage Needs Hyperdrive or an HTTP proxy โ€” Workers allow only 6 outgoing connections per invocation
Point-in-time recovery Time Travel, 30 days (Workers Paid) Provider-dependent, typically 7 to 35 days
Tooling and ecosystem Thin: Wrangler plus a few D1-aware ORMs Thirty years of tooling; engineers who already know it

Pricing, as of July 2026

D1 bills on a fundamentally different model from a Postgres instance. There is no hourly charge for a machine that sits idle. You pay for rows read, rows written, and storage.

Metric Workers Free Workers Paid
Rows read 5 million / day First 25 billion / month included, then $0.001 per million rows
Rows written 100,000 / day First 50 million / month included, then $1.00 per million rows
Storage 5 GB total First 5 GB included, then $0.75 per GB-month
Egress / data transfer No charge No charge
Database compute No charge No charge (the Worker that queries D1 is billed under normal Workers pricing)

Note the asymmetry: writes cost 1,000 times more per row than reads. A write-heavy, read-light system is the worst possible fit for D1 economics. And remember that indexes add writes โ€” an insert into an indexed column writes to the table and to the index.

The other detail people miss is what rows read means. It counts rows scanned, not rows returned. A SELECT that filters on an unindexed column of a 5,000-row table and returns one row still bills 5,000 rows read. Your bill is a direct function of how good your indexes are.

A worked example: a multi-tenant SaaS with 5,000 tenants

Say you run a B2B SaaS that manages inventory for retail chains. You have 5,000 customers. Each customer is a hard tenant boundary โ€” no customer ever needs to see another customer data, and none of them want to. Your largest tenant holds about 2 GB; the median tenant holds 50 to 300 MB.

The database-per-tenant shape

This is exactly the shape D1 was built for. You provision one D1 database per tenant โ€” 5,000 databases, comfortably inside the 50,000-per-account limit โ€” and your Worker routes each request to the correct tenant database based on subdomain or a JWT claim.

What you get for free is physical data isolation. No bug in a WHERE clause can leak tenant A data into tenant B response, because the data is in a different database entirely. If you have ever written row-level security policies in Postgres and then lain awake wondering whether you got every one of them right, you will appreciate the difference. You also get a much smaller blast radius: a tenant that runs a pathological full-scan query starves only its own shard, not the shared instance every other customer depends on.

Rough cost

Suppose the whole system does 2 billion rows read per month, 20 million rows written per month, and stores 400 GB in total. At July 2026 pricing, both the reads and the writes fall inside the Workers Paid included allowances (25 billion and 50 million respectively). Only storage bills: 400 GB minus the 5 GB included, at $0.75 per GB-month, is about $296 per month. Add the $5 Workers Paid subscription and your Worker request charges.

The point is not that this beats RDS on the raw number โ€” it might not. The point is that there is no idle capacity to pay for, no connection pooler to run, no failover to design, and nobody has to think about vacuum.

What you now have to build yourself

This is where honesty matters. With 5,000 databases, a schema migration is not one migration โ€” it is 5,000 migrations. You need a migration runner that iterates over every tenant, tracks which schema version each database is on, retries failures, and can roll back partially. That system does not exist for you. You will write it.

Cross-tenant reporting is the other cost. You cannot join across D1 databases. If your product team wants a dashboard showing the top-selling SKUs across the entire platform, you need a pipeline that exports data to somewhere else โ€” R2 plus an analytics engine, or a dedicated Postgres instance used purely for analytics. Budget that work explicitly at design time, not after the first executive asks for the report.

When PostgreSQL is the right answer

This is the section that matters most, because the cases where D1 is the wrong choice are more common than Cloudflare marketing will lead you to believe.

One large dataset that exceeds 10 GB

If your domain does not have a natural partition key โ€” a social network where every user is connected to every other, a marketplace whose search has to span every seller โ€” you cannot shard it without inventing distributed joins. That is hard, and you will do it worse than Postgres already does it. Here, 10 GB is a wall, not a challenge.

Heavy joins and analytical queries

SQLite is excellent at indexed lookups. It is not excellent at seven-table joins with window functions and nested CTEs. The PostgreSQL query planner is dramatically more sophisticated, keeps better statistics, supports parallel query execution, and gives you an EXPLAIN ANALYZE output that actually tells you what happened. If your business lives on reporting queries, D1 will make your life worse, not better.

Extensions: PostGIS, pgvector, TimescaleDB

A logistics system that needs nearest-driver-within-3-km queries needs spatial indexing. A RAG system that needs the closest documents to an embedding needs vector indexing. An IoT platform storing time series wants hypertables. D1 has none of this, and will not, because SQLite does not have a PostgreSQL-style extension system. This is not a gap waiting to be filled. It is an architectural difference.

Team and tooling

The least technical argument is often the decisive one. If your team has a DBA who knows Postgres deeply, monitoring already wired up, and runbooks for the incidents you actually see, moving to D1 throws all of that away and starts over. Cloudflare gives you Time Travel with 30 days of point-in-time recovery, which is genuinely good. But the tooling ecosystem around D1 is thin by comparison, and debugging a slow query is harder when you have fewer instruments.

Write-heavy and read-light workloads

Look at the pricing again. Rows written cost $1.00 per million. A system ingesting tens of millions of rows per day โ€” telemetry, clickstream, sensor data โ€” will run up a serious D1 bill while a mid-sized Postgres box does the same job for far less. Those workloads belong on Postgres, or in R2 and an analytics store.

The middle path most teams land on

D1 or Postgres is usually the wrong question. The better one is: which data goes where?

The shape that works: per-tenant operational data that must be fast and strictly isolated lives in D1, one database per tenant. The cross-tenant data โ€” the stuff you need to join, analyse, or run extensions against โ€” stays in the PostgreSQL you already have, and your Worker reaches it through Hyperdrive. Hyperdrive exists precisely to solve the connection-pool exhaustion you would otherwise hit, given that a Worker gets only 6 outgoing connections per invocation, and it caches eligible read queries on top.

If you are still deciding whether to put compute on Workers at all, we have written about when not to use Cloudflare Workers, and if you are comparing serverless platforms head to head, see Cloudflare Workers vs AWS Lambda.

A decision checklist

Answer these four before you commit. If you cannot answer one of them, you are not ready.

First: what is your shard key? If the answer is I am not sure, stay on Postgres. Second: how big will your largest single shard be in 24 months? If it is anywhere near 10 GB, D1 is not the answer for that data. Third: do you use any Postgres extension or Postgres-specific type? Go read the schema, do not guess. Fourth: how many rows will you write per month? Multiply by $1.00 per million and put that number in the application budget.

Cipher designs and implements systems on the Cloudflare Developer Platform for businesses in Thailand. If you are weighing D1, Hyperdrive, and your existing PostgreSQL against each other, we can assess the architecture and tell you honestly which parts belong where โ€” including the parts where the answer is: leave it on Postgres.

Frequently Asked Questions

Can a D1 database be expanded beyond 10 GB

No. Cloudflare documentation states explicitly that the 10 GB per-database limit on the Workers Paid plan cannot be further increased. The only path is to split your data across multiple databases, for example one database per tenant, which is the pattern D1 was specifically designed for. Workers Paid accounts support up to 50,000 databases, and that number can be increased on request.

Can D1 use PostGIS or pgvector

No. D1 runs SQLite, not PostgreSQL, so the PostgreSQL extension system does not exist there. If your system needs geospatial indexing or vector search, keep that data in PostgreSQL and have your Workers reach it through Hyperdrive.

How much traffic can one D1 database handle

Cloudflare documents that each D1 database is single-threaded and processes queries one at a time, so throughput is directly tied to query duration. At an average of 1 ms per query you get roughly 1,000 queries per second; at an average of 100 ms per query you get roughly 10 per second. Indexing is therefore a throughput concern, not just a cost concern.

What has to change when migrating from PostgreSQL to D1

You must convert the schema to SQLite, which means removing extensions, PL/pgSQL stored procedures, materialized views, and Postgres-specific types such as jsonb and arrays. You then have to design a sharding scheme that keeps every database under 10 GB. If you cannot state your shard key, you are not ready to migrate.

How does D1 billing work

As of July 2026, the Workers Paid plan includes 25 billion rows read and 50 million rows written per month. Beyond that, rows read cost $0.001 per million and rows written cost $1.00 per million. Storage includes 5 GB, then costs $0.75 per GB-month, and D1 charges nothing for egress.

Scroll to Top