ARCHITECTURENOV 21, 20256 MIN

Why we chose Cloudflare D1 over Postgres for TaxCalc

Relational data at the edge is tricky. Here is how we handled consistency without sacrificing the roughly 10ms backend budget.

Back to feedProduct updates/LogsArchitectureEdge
Axiom Labs

Axiom Labs

Field notes from the studio

Log 004 - Architecture // Edge SQL for sub-10ms tax calculations

What TaxCalc.ng really needed

We built TaxCalc.ng with a hard constraint: a typical calculation should feel instant, even on a shaky Nigerian 4G connection. End-to-end latency had to stay under ~200ms, while our own backend/database work for the calculation path had a roughly 10ms budget.

The product is a focused financial app: users, sessions, saved calculations, Paystack payments, and audit trails. It wants relational modeling, transactions, constraints, and low operational overhead.

The database checklist was simple: SQL with joins and transactions, global performance for Lagos and London, and enough consistency to keep money right even if some UI paths are optimistic.

Why 'just use Postgres' wasn't enough

A single Postgres primary region adds hundreds of milliseconds of round trips for Workers running in faraway POPs; that alone blows the roughly 10ms backend budget.

Managed clusters, poolers, tunnels, and multi-region stories are solvable but shift time from "build TaxCalc.ng" to "babysit infra."

Data volume is small-to-medium. Postgres headroom was overkill for v1 while the operational cost was very real.

Why D1 made sense for TaxCalc.ng

Cloudflare D1 mounts SQLite right next to the Worker, so reads and writes avoid the extra network hop to a central database.

No connection pooling, no long-lived sockets; just the D1 binding and Drizzle ORM. Our stack stays on one platform: Next.js on Pages, Workers for API, D1 for data.

We still get SQL, foreign keys, and per-request transactions, which covers TaxCalc.ng's data model without spinning up extra infra.

The consistency problem we had to solve

Writes do not instantly exist everywhere. A user could upgrade to Premium in one region and still look Free in another for a few seconds.

Overlapping writes from different Workers or retried Paystack webhooks can race each other. Financial data cannot tolerate silent duplication or lost updates.

We split invariants into "hard" (payments, entitlements) and "soft" (badges, counters) so we could design for eventual consistency without risking money.

Guardrails we layered on top of D1

Idempotent payment handling: every Paystack reference is unique and enforced in the schema. Webhook retries become no-ops instead of double-charges.

Application-level locks on critical flows: either routing certain writes to a single colo or using version/nonce columns so conflicting updates are rejected and retried.

Treating non-critical flags as caches: JWT premium flags or last-seen summaries can drift briefly; audit trails remain the source of truth.

What we gained and accepted

Single-digit millisecond DB calls in practice and a unified Cloudflare deployment story.

Sufficient relational power for TaxCalc.ng with minimal operational overhead.

We accepted eventual consistency quirks, added app-level safeguards, and acknowledged that migrating to a heavier database later is a worthwhile future project if growth demands it.

Key takeaway

We did not pick "the best database"; we picked the one that met the backend budget and designed around its constraints.

Useful? Send it to someone scoping a similar build.

Related Signals

Stay in the loop

Follow the Studio

More field notes and build logs.