Postgres alongside D1
A managed Postgres database per app for workloads D1 doesn't fit: how it connects, the four limits that shape the design, what it costs, and an honest decision rule for choosing between the two.
Enterprise capability
Postgres is an enterprise capability, provisioned under an enterprise agreement rather than enabled by default. D1 remains the default on every tier, including enterprise. If a Postgres requirement is load-bearing for your project, raise it before you build — the data-model decision is expensive to reverse.
Some workloads don't fit D1: a dataset past its 10 GB per-database ceiling, a Postgres extension you depend on, or a regulatory requirement that the data sit in a named country. For those, an app can use a managed Postgres database alongside D1.
How it connects
Postgres is reached over a Hyperdrive binding, which pools and routes connections at the edge:
Two consequences worth noting immediately. Your Worker never holds the database credentials — it receives a Hyperdrive-local connection string, and the origin credentials stay in the configuration. And an app can use both: D1 for what suits D1, Postgres for what doesn't, in the same codebase.
The four limits that shape the design
These aren't footnotes; each one changes how you write the code.
| Limit | Value | What it means for you |
|---|---|---|
| Connections per Worker invocation | 6 concurrent TCP | Configure the driver pool below that. Don't fan out parallel queries per request |
| Query duration | 60 s ceiling | Long analytical queries need a different home |
| Pooled origin connections | ~100 on paid tiers | The real concurrency ceiling under load |
| Query cache | On by default, ~60 s | Breaks read-your-writes — see below |
The caching trap
This is the one that produces a bug report rather than an error:
The connection layer caches read queries by default. A user saves a record, the next read hits a cached result from before the write, and they're told their change didn't save.
The fix is configuration, not code: disable caching on the configuration used for transactional reads, or provision a second uncached binding and route read-your-writes traffic to it. The pattern parallels D1's session bookmarks, but the mechanism is different and it does not happen automatically. See Consistency and correctness.
Also true
- Your Worker needs Node.js compatibility and a Postgres driver, and you manage a pool per request — D1's session middleware pattern doesn't map.
- The number of databases is capped by configurations per account, roughly 25 by default and raisable by request — against D1's 50,000. That cap, not the Postgres provider, is the per-app scaling bottleneck, so a fleet of many small Postgres databases is the wrong shape.
Choosing between them
The honest decision rule:
| D1 | Postgres | |
|---|---|---|
| Availability | All plans | Enterprise agreement |
| Cost floor | None beyond your plan | ~$5/month per database |
| Size ceiling | 10 GB per database | Far higher |
| Databases per account | 50,000 | ~25 configurations (raisable) |
| Point-in-time recovery | 30 days, built in | Provider-dependent — verify during scoping |
| Region control | Not selectable | Selectable |
| Read replicas | Yes, with session bookmarks | Provider-dependent |
| Read-your-writes | Automatic via session | Manual — disable query caching |
| Per-environment separation | Automatic | One database per environment, provisioned |
| SQL | SQLite dialect | Full Postgres |
Two rows deserve emphasis because they cut against the assumption that Postgres is simply the more capable option:
D1 has better recovery. A 30-day point-in-time rewind, built in, no configuration. Postgres recovery depends on the provider's mechanism and window. If "prove you can restore last Tuesday" is a requirement, D1 is currently the stronger answer.
D1 scales to more databases. Fifty thousand versus roughly twenty-five configurations. For an app-per-tenant or database-per-customer architecture, D1 is the only one of the two that fits.
What Postgres unlocks: data residency
Worth calling out separately, because it's the requirement we're asked about most and the standard platform doesn't satisfy it.
Compute placement is not controllable — your code runs in the Cloudflare location nearest each user, on every tier and in every deployment model. But a Postgres database is provisioned in a region you choose, which makes "customer records are stored in the EU" an answerable requirement rather than an unmet one.
Scope this precisely. "Data residency" means different things in different policies — storage location, processing location, or both. Compute still runs globally, so if your obligation covers processing and not just storage, bring the exact wording to the conversation. We'd rather tell you it doesn't fit than discover that during an audit.
Using both in one app
The conventional shape once both exist, and a good default:
- Postgres holds the large or regulated table — the transaction ledger, the customer records under a residency obligation.
- D1 holds everything else, and keeps the operational advantages: point-in-time recovery, per-environment provisioning that needs no setup, and the app's own event feed.
- The edge cache and KV serve read traffic in front of either, so neither database carries load it doesn't need to.
The rule from Choosing a data store still holds: put each piece of state where its guarantees belong, not where the most powerful engine is.
Next
- Choosing a data store — the full picture including Durable Objects, KV, and R2.
- Deployment models — the other enterprise capability.
- Consistency and correctness — why the caching default matters.
Frequently asked questions
Is Postgres replacing D1?
No. D1 remains the default and is the right choice for most applications. Postgres is an opt-in addition for datasets beyond D1's 10 GB per database, for Postgres-specific features, or when you need a database in a region you choose.
How does a Worker reach Postgres?
Through a Hyperdrive binding, which pools connections at the edge and hands your Worker a local connection string. Your code uses an ordinary Postgres driver; the origin credentials live in the Hyperdrive configuration, not in your app.
Does it have point-in-time recovery like D1?
Recovery comes from the Postgres provider rather than from D1's Time Travel, so the mechanism and window differ. Treat it as a separate recovery posture to verify during scoping, not as an assumed 30-day rewind.
What does it cost?
There's a floor of roughly $5 per month per database on the smallest single-node tier, before usage. That floor is exactly why it's opt-in: for most apps D1 costs nothing comparable and does the job.
What's the biggest gotcha?
Query caching. The connection layer caches reads by default, which breaks read-your-writes — a user can save something and not see it. Configure caching off for anything transactional, or use a separate uncached binding for those reads.