Lately I keep reaching for SQLite where, before, I'd have reached for Postgres without thinking.
It started with small services, then a bigger question: could a multi-tenant SaaS actually run on SQLite? And for AI agents specifically, isn't a local, embedded database the more natural home for their state?
Turso is the version of this stack I've found most compelling so far, especially when paired with Cloudflare. I wish D1 would reach embedded-replica parity with Turso, and that AWS offered a managed SQLite-style service the way it offers RDS for Postgres.
This isn't a "Postgres is over" argument. I still use Postgres more often than SQLite. And it isn't advice. It's just where my thinking has drifted recently β written down mostly so I can find out where it's wrong. Read it as one person's notes, not a recommendation.
Where I've landed for now (and expect to keep revising):
- SQLite isn't replacing Postgres.
- For work state, it's increasingly my first reach, not my last.
- AI agents push this harder: their state is high-churn, local, and mostly private.
- The answer isn't all-local. It's a local workbench plus a central ledger.
Why the old default existed
For years, "where does the data live?" had one practical answer: a server, behind an API, in a shared Postgres. A lot of that wasn't architecture β it was the cheapest shape available.
SQLite was already everywhere, but it lacked the operational layer that makes a database viable as SaaS infrastructure: networking, replication, managed backups, and a way to run many small databases without drowning in tooling. So centralizing was the path of least resistance, and a tenant_id column in shared Postgres became the reflex.
What changed isn't SQLite. It's that the ecosystem grew the missing parts β and for a growing class of workloads, the thing doing the most frequent writing moved onto my own machine.
The constraint that's lifting
SQLite itself is, by design:
- Embedded, not networked β a library, nothing listens on a port.
- Single-file, not replicated β no built-in read replicas or failover.
-
Single-writer β WAL gives many readers + one writer; contend and you hit
SQLITE_BUSY.
None of this got fixed by changing SQLite. It got fixed around it:
- libSQL (Turso's SQLite-compatible fork) β managed server layer, remote access, embedded replicas, encryption, vector search. (The from-scratch Rust successor β "Limbo," now Turso Database β is a separate, less-mature track; don't conflate them.)
- Embedded replicas β a full local SQLite copy synced into your process; reads local, writes forwarded to a primary.
- Cloudflare D1 β managed serverless SQLite with global read replication and read-your-writes via a Sessions API.
- Durable Objects β a SQLite database in the same thread as your code.
- Litestream / LiteFS β stream the WAL to object storage for durability, without touching the write path.
The single-writer model hasn't disappeared, and I still design around it. But "SQLite can't be networked, replicated, or run as shared infrastructure" has stopped being quite as true in practice β so some of the reasons I used to centralize by reflex don't hold for me the way they once did.
State gravity
A loose model I use to keep the trade-off straight: state gets pulled toward whatever reads and writes it most. Put the writer far from the state and you pay the distance on every op β latency, egress, pools, consistency reconciliation. Put them close and most of that disappears.
For two decades the writer was a human clicking a web app: small, occasional, round-trip-tolerant. Weak pull, so central state was fine.
That writer changed. The useful agents run inside the work environment β file system, terminal, IDE, browser tab, an already-authenticated session. Codex CLI runs in a local terminal against a directory you choose; Claude Code reads the codebase, edits files, and runs commands on your machine. The strength is proximity as much as model quality.
While it works, an agent holds a lot of state: task plan, file index, staged/unstaged diffs, tool-call history, failures and retries, approval state, a local search index, an embeddings cache, sync cursors, idempotency keys, an undo/redo log.
It's high-churn, local, and useless to anyone else. Round-tripping all of it to central Postgres is expensive on three fronts:
- Latency β local SQLite reads tend to be one to several orders of magnitude faster than a networked read (workload-dependent): an embedded replica answers from a local file; a remote read pays the network first.
- Cost β ledger-grade infrastructure to store scratch work deleted in an hour.
- Privacy β the moment an agent's index of your files leaves the machine, you've created a data-handling problem you didn't need.
Writer moves local β work state follows.
Local isn't just closer β it's bigger
Proximity is one reason. The one I find most underrated is raw hardware β but I want to be careful about which case it applies to, because it's easy to overreach here.
It applies to the resident agent running on the user's own machine β Claude Code, Codex, and the like, running as desktop/CLI apps, a distinctly desktop-shaped workload. Compare what each side gives that single user. A modern laptop, exclusively: several to a dozen-plus CPU cores, tens of GB of RAM, a fast NVMe SSD, the local file system, sometimes a GPU/NPU, and a session already authenticated as them. A cloud multi-tenant runtime would hand that same user a slice β capped CPU, memory, disk I/O, persistent storage β and to run tools safely server-side you'd also need sandboxes, VMs, network controls, permissioning, audit logging, queueing. So even before network latency enters the picture, the local machine often wins on resources per user; the network just widens the gap. Let the model think in the cloud β the reading, writing, diffing, indexing, searching, and caching are cheaper where the hardware actually is, and that's the work that generates the local state.
It does not automatically carry over to a server-side multi-tenant SaaS agent, where the agent runs in your own infrastructure rather than on the user's laptop β there's no laptop in the loop to be "bigger," so this particular argument simply doesn't apply. The case for SQLite there rests on something different: the per-tenant economics, isolation, and blast-radius properties in the next section β not on "the edge has more compute." Two separate arguments for two separate cases, and I don't want to smuggle one in to prop up the other.
database-per-tenant, reconsidered
The pattern I keep arriving at for SaaS: one SQLite database per tenant (or per user, per agent), instead of shared Postgres with tenant_id everywhere and RLS holding isolation together.
Because a database is a file, the economics flip:
- Tenant #10,000 is just another file β no per-database process, no cold start.
- Cost scales with usage, not database count. In Turso's model, database count isn't the scarce unit it would be with Postgres; D1's current limits allow up to 50,000 databases/account, with billing based on rows + storage rather than a per-database server.
- Isolation is physical, not a
WHEREclause you hope you never forget. - Backup, PITR, and export become per-file operations. "Delete this customer" gets close to a per-file action β not literally
rm, since backups, logs, the warehouse, search indexes, and audit trails still matter, but the operational state isn't scattered across shared tables. - Blast radius is bounded β a bad migration hits one file, not the shared cluster.
The honest costs:
-
Cross-tenant queries get harder. Not solved by querying 10,000 files at runtime β solved by
outboxper tenant β event stream β central warehouse. That's infra you now own. - Migrations fan out. N migrations with partial-failure states; you want tooling that treats "apply to all tenant DBs" as a first-class, resumable op.
- Single-writer ceiling is per database. Usually fine. A genuinely write-saturating tenant needs a different home β and isolation means only that tenant does.
- D1 shape limits β 10 GB per database, single-threaded query execution per database.
The trade: give up effortless cross-tenant analytics and single-migration simplicity; get physical isolation, flatter cost, simple per-tenant lifecycle ops, bounded blast radius. For a lot of the B2B SaaS I work on, that's felt like a good trade lately β though how it nets out depends heavily on the workload, and I wouldn't generalize it past that.
Two latency tiers
PRIMARY (source of truth)
β WAL frames
βΌ
EDGE REPLICAS ββββ regional, network read
β
βΌ
EMBEDDED REPLICA ββ in your process, local file read
β
βββ writes ββββββββββββββΆ forwarded to PRIMARY
- Regional read replicas (D1, Turso cloud) put data near the user. D1's read latency can be excellent near the Worker β but it's still a network read.
- Embedded replicas (Turso/libSQL) sync a real SQLite file into the process, so reads never leave the box. This is the tier I want for agents, which read their own state constantly.
In both, writes route to a single primary β the single-writer rule reappearing as a write-latency floor. US-West primary + Singapore writer = a Pacific round trip, regardless. You design for read-locality and accept write-centralization, which conveniently matches "local work state, central facts."
Consistency isn't free: D1's Sessions API gives sequential consistency within a session and read-your-writes via bookmarks. Adopt replicas, adopt their staleness model on purpose.
The agent's database = workbench + memory
- Colocated compute + data. A Durable Object is a named unit of compute with its own strongly-consistent, SQLite-backed storage: "this agent owns this workspace" becomes literal β one object, one database, one writer, on demand.
- Vector search in the same DB. Turso ships native vector search alongside relational columns, so for agent memory and RAG, embeddings and records live together β no separate vector DB to sync.
Scratch state in local SQLite, an embedded replica for fast reads of shared state, vector search in-box for memory. Little of that wants to be a network call to central Postgres.
Objections I'd raise myself
- "SQLite still has one writer." True. I'm not claiming high write concurrency β I'm claiming many workloads don't need it inside one database once you pick the right boundary. It's a reason to choose boundaries well, not to centralize everything.
-
"Postgres does multi-tenant cheaply too." It does β Neon, Aurora Serverless, Supabase, RLS. The point isn't "Postgres is bad" β it's that, for me, "shared Postgres +
tenant_id" stopped being the automatic starting point and became one option I weigh among others. - "Local-first sync is hard." The load-bearing difficulty. Sync is a product surface, not a background job. If two actors edit the same object, you need a conflict model before a database opinion. Per-tenant is easiest when tenants don't share writable state.
-
"Cross-tenant analytics gets painful." Yes β you lose the free
JOINand buy it back with a warehouse. Analytics-first across all tenants? Shared Postgres may still win. Tenant-isolated with roll-ups? Per-tenant tends to win.
Where it's not all-SQLite
Three things keep a strict central store:
- Collaboration β shared, concurrently-edited objects need sync, conflict resolution, permissions, a converged view. Local SQLite provides none of that.
- Accountability β who instructed it, which model, what permissions, what changed. That trace can't live only on the laptop it happened on.
- Global truth β money, inventory, billing, contracts. Spread across edges and the system can disagree with itself about how much money exists. The ledger stays central, strict, singular.
So the shape is a gradient β fast/disposable at the edge, slow/strict at the center:
SYSTEM OF WORK β the workbench (local Β· high churn Β· disposable)
ββββββββββββββββββββββββββββββββββββββββββββββββ
β Local SQLite agent / device work β
β Embedded replica fast local reads β
β Per-tenant SQLite operational state β
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββ
β
β¨ Event log β sync + audit spine β©
β
ββββββββββββββββββββββββββΌββββββββββββββββββββββββ
β Central ledger (Postgres) money Β· billing β
β inventory Β· contractsβ
β Warehouse / search cross-tenant analyticsβ
ββββββββββββββββββββββββββββββββββββββββββββββββ
SYSTEM OF RECORD β the vault (central Β· slow Β· strict)
Postgres doesn't go away. It stops being where I start, and its free cross-tenant analytics moves to the warehouse.
A test: trace one change
An agent on a laptop edits a customer-facing record. What did it touch? On whose authority? Against what state? Can the team see it without re-deriving it?
- All-central β clean answers, paid for by routing every keystroke (mostly scratch) through the network. Ledger cost for workbench state.
- All-local β what and against what are fast; authority and visibility have no home.
- Gradient β scratch stays in local SQLite; the change that mattered flows through the event log into the ledger, where it's authorized, audited, shared.
What's still missing in 2026
- Turso/libSQL β the most complete realization I've used: managed DBs, embedded replicas, vector search, per-tenant economics, open-source core (self-host exit). Build on the production C libSQL; treat the Rust successor as a separate track.
- Cloudflare D1 β great if you live on Cloudflare: zero-ops, built-in read replication, per-tenant at scale, composes with Workers/DO/R2. What I miss is the embedded-replica model β D1 reads stay network reads. D1 reaching embedded-replica parity is the upgrade I'd most like to see.
- Durable Objects β the per-entity colocated-SQLite primitive; storage billing for SQLite-backed DOs starting January 2026 reads as a real product line.
- AWS has no first-class managed SQLite/libSQL tier β nothing comparable to RDS-for-Postgres or D1/Turso for SQLite workloads. You can assemble it (EFS/S3 + Litestream, SQLite on an ephemeral volume, rqlite on EC2), but there's no managed, replicated, embedded-replica-capable service with RDS-grade polish. For AWS-standardized teams, that's the biggest reason this is harder than it should be.
The pattern feels right to me, libSQL/Turso seem to show it working in production, Cloudflare shows a zero-ops version β and the major clouds, AWS most of all, haven't shipped the managed tier that would make adopting it boring.
Design for the gravity
It compounds. As the desktop gets stronger, the browser follows: WASM, OPFS, Web Workers, WebGPU turn the tab from a display surface into a local runtime β which is why a real SQLite file persisting inside a browser tab stopped being exotic. Stronger local execution pulls more state local, and local state pulls SQLite in.
Desktop agents, the browser becoming a runtime, database-per-tenant, the SQLite revival β to me these don't feel like separate trends so much as one shift seen from different angles: the writer moving back to the edge, the tooling finally giving the edge a real database, and state following. It all feels connected β though that's a hunch I hold loosely, not a thesis I'd defend to the death.
Postgres can be the vault again. SQLite, per tenant and per agent, becomes the workbench. An event log carries the difference.
That's the pull I keep feeling β designing for it a bit more, out of habit a bit less, and still figuring out the edges.
Top comments (0)