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.

The Spikefrost Team31 Jul 20265 min read

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:

Postgres reached through a Hyperdrive binding. The Worker gets a local connection string; origin credentials live in the Hyperdrive configuration, never in application code.

pg driver

pooled TCP

Worker
your app code

Hyperdrive binding
connection pool + optional cache

Managed Postgres
region you choose

D1
still available in the same app

Postgres reached through a Hyperdrive binding. The Worker gets a local connection string; origin credentials live in the Hyperdrive configuration, never in application code.

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 is the default. Postgres earns its place on four specific requirements, not as a general upgrade.

yes

no

yes

no

yes

no

yes

no

Where should this data live?

More than 10 GB
in one database?

Postgres

Need Postgres extensions,
types or SQL dialect?

Must the data sit in a
specific country?

Existing Postgres schema
you're migrating as-is?

D1 — the default

D1 is the default. Postgres earns its place on four specific requirements, not as a general upgrade.
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

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.