Written by Huifer, solo developer and maintainer of TanStack Ship. I run Cloudflare D1 across four production SaaS apps (max 2,400 rows on the heaviest) and Postgres-on-Neon across three production B2B apps (max 11M rows on the heaviest) over 18 months. This is the honest D1 vs Postgres comparison I wish I had before app #1: real production limits, real production pricing, a decision matrix mapped to SaaS shape, and an honest graduation path. No vendor sponsorship; every claim links to a first-party source.
Verified sources: Cloudflare D1 docs · D1 limits · D1 best practices · D1 pricing · Neon serverless Postgres · Neon pricing · PostgreSQL official docs · SQLite query planner · Workers Analytics Engine · Drizzle ORM D1 driver · Last updated: 2026-05-09 · Changelog
TL;DR: D1 is the right default for a Workers-native read-heavy SaaS in 2026: sub-millisecond reads from regional replicas, no connection pool to manage, SQLite's query planner, single billing line. Postgres-on-Neon is the right choice when you need
JSONB,LISTEN/NOTIFY, full-text search out of the box, or>10 GBdatabases. Both have honest limits — D1's 10 GB database cap and write-lock contention; Neon's ~1.2 s cold-start and egress cost on cross-region reads. The article ends with a decision matrix and an honest graduation path. For the deep-dive benchmark first, see the D1 performance for SaaS post; for the production patterns, see the D1 production guide.
Cloudflare D1 vs Postgres: The 18-Month Production Comparison
The "D1 vs Postgres" question has no single answer, but it has a small number of honest ones. Cloudflare D1 is one; Neon serverless Postgres is the other I actually ship with on production B2B. Both engines are acknowledged on their strongest axis, then where they actually lose, with a decision matrix and graduation path at the end.
The Production Database Decision in 2026
Two forces have reshaped the database landscape for a solo SaaS in the last three years. Edge runtimes — Cloudflare Workers first and foremost — have moved compute close to users. SQLite has become a distributed-systems primitive, with D1 turning a single-file engine into a globally replicated database with sub-millisecond reads.
For a SaaS founder on Workers, D1 is the default and Postgres is the graduation target — not because Postgres is "older", but because the two engines solve different shapes of problem at different price points. The comparison below is from running both engines in parallel on paid SaaS traffic.
Cloudflare D1: The Edge-SQLite Bet
D1 is SQLite 3.46+ with HTTP-level bindings, automatic backups to Cloudflare R2, and read replicas in every region Cloudflare runs.
Strengths: where D1 wins
D1's four production advantages have held across all four of my deployments:
Edge latency, zero pool
A D1 read from a Worker in the same region is sub-millisecond. There is no connection pool — each Worker invocation is a single short-lived SQLite open that closes when the request ends. The pattern holds for cold Workers and warm Workers alike.
Predictable billing
D1 pricing is $0.75/GB-month for stored rows and a small included reads/writes band per account. None of my four D1 deployments have generated a meaningful storage bill; the request cost is the only line that moves.
SQLite tooling everywhere
The same sqlite3 CLI you run locally works against a D1 backup. The SQLite query planner is the fastest query planner I have measured for read-heavy OLTP. sqlite3 my.db and you are in.
No egress cost
Cross-region replica reads are free inside the Workers ecosystem. That changes the unit economics of any feature that fans out geographically — multi-tenant dashboards, country-specific pricing, per-region search indexes.
Limits: where D1 actually loses
10 GB database cap
Per D1 limits, a single D1 database is capped at 10 GB. For B2C with media catalogs this matters; for B2B it almost never does. The cap is on database size, not working set, so a slimmer schema with archived rows can stretch well past 50 million rows.
Write-lock contention
A single D1 database serializes writes through one writer. Sustained write-heavy workloads (>50 writes/sec aggregated) see latency tail and queue depth at the writer. The mitigation is D1 best practices on batching — coalesce writes into fewer invocations, not more.
No JSONB, no LISTEN/NOTIFY
SQLite JSON1 is good but not Postgres JSONB. If your schema's value depends on GIN indexes over nested objects, Postgres wins. Same for LISTEN/NOTIFY change feeds — D1 has no first-party equivalent.
No native full-text search on par
FTS5 in SQLite works but needs explicit setup and is not on parity with tsvector + pg_trgm. Ranking quality on multilingual corpora is materially better on Postgres.
Postgres (Neon): The Serverless Postgres Prime
Postgres on Neon is the canonical "Postgres without managing a VM" choice. Full Postgres 16+; the runtime splits compute and storage so the database scales-to-zero when idle.
Strengths: where Postgres wins
Postgres' four production advantages over D1 have held across all three of my Neon deployments:
The full SQL surface
JSONB with GIN indexes, tsvector full-text search, LISTEN/NOTIFY for change feeds, window functions, lateral joins — anything you can write in standard Postgres works. The single biggest reason B2B SaaS graduates to Postgres.
Scale past D1's ceiling
Neon free tier scales to 0.5 GB; the smallest paid plan handles 100 GB+. The largest in my portfolio is an 11M-row events table that has run cleanly for 14 months.
Postgres-native tooling
The psql CLI works as expected. pg_dump and pg_restore produce dumps compatible with any Postgres 16 host, on-prem or otherwise. Debugging is deterministic.
Mature extensions
pg_trgm, pgvector, pg_cron, PostGIS — Postgres's extension ecosystem is the longest in the OLTP world.
Limits: where Postgres actually loses
Cold-start latency
A Neon database idle for 5+ minutes takes ~1.2 seconds to wake on first query. Auto-scaling helps; cold-start remains the longest tail in the latency budget. A low-cost "pinger" Worker that issues a no-op query every 4 minutes adds $0.02/month but removes the spike.
Egress cost
Cross-region reads route through the Neon proxy and incur egress charges. None of my deployments hit a meaningful egress bill, but the rate is non-zero and worth pre-budgeting.
Connection pool overhead
Even with Neon serverless, every Worker that hits Postgres needs a driver-level pool. For Workers specifically, Cloudflare Hyperdrive is the right pattern, and adds another moving part to the production stack.
JSONB is not free
JSONB is wonderful but misusing GIN indexes on it is the slowest schema pattern Postgres has. Compare JSONB queries to relational before committing.
Decision Matrix: D1 vs Postgres by SaaS Shape
Eight rows, three columns. The first column is the workload shape; the second is the recommended engine; the third is the honest reason.
| Workload shape | Recommended engine | Why |
|---|---|---|
| Read-heavy SaaS, < 10M rows, Workers-native | D1 | Sub-millisecond reads, zero pool, single billing line |
| Write-heavy SaaS, < 50 writes/sec | D1 | Single writer is enough; > 50 writes/sec and you feel the lock |
Schema with JSONB or GIN-indexed objects | Postgres | SQLite JSON1 is the wrong tool for deep object queries |
| Full-text search on user content | Postgres | tsvector + pg_trgm beat FTS5 in setup cost and ranking quality |
| Real-time change feed to clients | Postgres | LISTEN/NOTIFY is one query; on D1 you wire it manually |
>10 GB database or B2C media catalog | Postgres | Past D1's 10 GB cap; Neon's smallest plan handles it |
| Cold-start-sensitive user-facing API | D1 | Sub-millisecond reads beat 1.2 s Neon cold-start on the same query |
| Multi-region fan-out, low write rate | D1 | Free cross-region reads beat Neon's egress on the same workload |
The matrix collapses to two rules: if the workload shape fits in SQLite and you run on Workers, pick D1. Everything else graduates to Postgres.
When to Graduate from D1 to Postgres
The graduation triggers I have used in production, in priority order:
Database size approaching 10 GB
When rows + indexes passes 60% of the 10 GB cap (~6 GB), start the Postgres sidecar — D1 reads stay live while Postgres takes new writes. The data side is in the D1 backup and restore post.
Write contention showing up in latency tail
When p99 write latency passes 250 ms sustained, the lock is the bottleneck. Postgres-on-Neon with a pool absorbs the same write load at 30–50 ms p99.
New feature needs JSONB, FTS, or LISTEN/NOTIFY
The cleanest trigger. Build the new feature on Postgres from day one; the rest of the app stays on D1 until the next migration. Most B2B apps live in this "polyglot" state for 6–12 months.
Compliance requires Postgres encryption modules
Some regulated industries (HIPAA, certain EU workloads) require Postgres's row-level security or pgcrypto at the database layer. D1 has neither. Rare as a trigger for solo SaaS, but it shows up in B2B sales conversations.
A Worked Production Example
Same schema, two engines. The query returns "tenants with most recent paid event":
D1 schema and query
-- D1: SQLite dialect, tenant_id first, integers for time
CREATE TABLE projects (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
name TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX idx_projects_tenant_created
ON projects(tenant_id, created_at DESC);
CREATE TABLE events (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
kind TEXT NOT NULL,
amount_cents INTEGER NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX idx_events_tenant_kind_created
ON events(tenant_id, kind, created_at DESC);
-- The query
SELECT
p.tenant_id,
p.name,
MAX(e.created_at) AS latest_paid_at
FROM projects p
JOIN events e
ON e.tenant_id = p.tenant_id
WHERE e.kind = 'paid'
GROUP BY p.tenant_id, p.name
ORDER BY latest_paid_at DESC
LIMIT 10;
Postgres-on-Neon schema and query
-- Postgres: real TIMESTAMPTZ, JSONB supported
CREATE TABLE projects (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
name TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_projects_tenant_created
ON projects(tenant_id, created_at DESC);
CREATE TABLE events (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
kind TEXT NOT NULL,
amount_cents INTEGER NOT NULL,
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_events_tenant_kind_created
ON events(tenant_id, kind, created_at DESC);
-- The same business query, with full JSONB access and FILTER
SELECT
p.tenant_id,
p.name,
MAX(e.created_at) AS latest_paid_at,
SUM(e.amount_cents) FILTER (WHERE e.kind = 'paid') AS total_paid_cents
FROM projects p
JOIN events e
ON e.tenant_id = p.tenant_id
WHERE e.kind = 'paid'
GROUP BY p.tenant_id, p.name
ORDER BY latest_paid_at DESC
LIMIT 10;
The same query shape, but Postgres gives FILTER, real TIMESTAMPTZ, and JSONB for free. D1 keeps you on INTEGER Unix timestamps and forces you to do that FILTER work in application code. With Drizzle ORM, the same TypeScript schema definition compiles to both dialects.
Where Each Engine Wins, Honestly
Before the CTA, the unambiguous cases:
- D1 wins for Workers-native read-heavy SaaS. Sub-millisecond edge reads, zero connection pool, predictable billing — four paid products in my own portfolio.
- D1 wins for cross-region fan-out workloads. Free cross-region replica reads with no egress is a unit-economics advantage Postgres-on-Neon cannot match.
- Postgres wins for schema-heavy analytics. If the business shape benefits from
JSONB,tsvector,LISTEN/NOTIFY, or any of the 200+ Postgres extensions, the migration cost is paid back by the cleaner schema. - Postgres wins for
>10 GBor write-heavy workloads. Past D1's 10 GB cap or 50-writes/sec ceiling, Postgres-on-Neon or another managed Postgres is the simpler operational shape.
Where TanStack Ship fits: a solo dev wants D1 as the default with a Postgres-ready stack — schema patterns, Drizzle ORM migrations, and the documented graduation path all wired in. TanStack Ship ships D1 with the same first-class story you'd expect for Postgres, including drizzle.config.ts, the migration runner, and the matrix above as a runbook.
FAQ
Is D1 production-ready in 2026?
Yes for read-heavy workloads under the 10 GB cap. All four of my production D1 deployments have run 18+ months with measured downtime under the Cloudflare Workers SLA. Write-heavy workloads above 50 writes/sec still benefit from Postgres.
How does D1 compare to Postgres on price?
Per D1 pricing and Neon pricing, a 1 GB D1 database with 5M monthly read queries is roughly $0.75/mo storage + $0.05/mo reads ≈ $0.80/mo. Same workload on Neon Launch is $0/mo until you exit free, then $19/mo. For low-write workloads D1 is materially cheaper; for high-write or large databases Neon wins.
Can D1 replicate globally automatically?
Yes, via the D1 read replicas feature. Writers are global through one primary; readers fan out to every Cloudflare region. This is the largest unit-economics difference between D1 and Postgres-on-Neon for cross-region traffic.
What happens to my TanStack Start app if D1 hits the 10 GB cap?
The query layer keeps returning results; new writes fail with StorageLimitExceeded. The recovery path is a Postgres sidecar that takes new writes while D1 stays read-only — the data side is in the D1 backup restore post. None of my apps have hit this in production, but the runbook is rehearsed.
Does D1 support transactions across tables?
Yes — every D1 query is automatically inside a transaction per Worker invocation when using batch statements via D1 best practices. Cross-request transactions require db.batch([...]) and run on the same write lock.
When should I pick Postgres over D1 for a brand-new SaaS?
If the schema needs JSONB, full-text search, LISTEN/NOTIFY, or is forecast to cross 10 GB in the first 12 months, start on Postgres. Otherwise D1 is the cheaper default. The decision matrix above is the same one I use for early-stage TanStack Ship customers who ask "D1 or Postgres?".