In 2024–2026 SQLite went from “good for tests and toy apps” to “deploy this in production for half the use cases you assumed needed Postgres.” The catalyst was a small constellation of technologies — Turso/libSQL, Litestream, Cloudflare D1, and embedded replicas — that solved SQLite’s two real limitations (no replication, no remote access) without giving up its absurd simplicity.
This post is the working knowledge: when SQLite wins in 2026, what tooling exists, and where Postgres still earns the call.
What SQLite is good at — and the parts you forgot
SQLite is not “Postgres lite.” It’s a different shape:
- In-process. Reads from a SQLite database go through the same memory bus as a hash map lookup. No socket, no parser-on-the-other-side, no deserialization tax.
- One file. The whole database is a file you can
cp,rsync, or attach to an email. - Reliable. It’s the most-tested codebase in the world. Powers iMessage, Firefox, every Android app, every airplane.
- Fast for reads. A single connection on a SSD reads 100k+ small rows per second. Multiple readers run in parallel without coordination.
The historical limits:
- One writer at a time (serialized at the file).
- No real network access — the database is on the same machine as the app.
- No built-in replication.
In 2026, all three of those have credible solutions.
The SQLite stack of 2026
| Component | Job |
|---|---|
| SQLite | The engine itself |
| WAL mode | Concurrent readers + one writer; crash safety |
| Litestream | Continuous replication of the WAL to S3-compatible storage |
| libSQL / Turso | SQLite fork with HTTP server, embedded replicas, multi-tenant primitives |
| Cloudflare D1 | Managed SQLite at the edge, integrated with Workers |
| LiteFS (Fly.io) | Distributed SQLite as a virtual filesystem |
| rusqlite, better-sqlite3, sqlx-sqlite | Native client libraries |
The flagship pattern is embedded replica with periodic sync: the application has a local SQLite file that pulls changes from a remote primary every N milliseconds. Reads are local (microseconds). Writes go to the primary.
When SQLite wins in 2026
1. Read-heavy SaaS
A B2B SaaS that’s 99% reads is the killer use case. With Turso embedded replicas:
- Reads: ~600 microseconds (local disk + memory).
- Writes: 5–50ms (network round-trip to primary).
Compare to managed Postgres: 1–5ms p50 reads after network. Embedded SQLite is 5–50× faster for the read-heavy path.
2. Per-tenant isolation
/data/
tenant-1.db
tenant-2.db
tenant-3.db
...
Each tenant gets a separate SQLite file. You get hard isolation, easy export (“here’s your data”), trivial backup, and per-tenant scaling. Turso makes this first-class with its “database per tenant” pricing.
A 50,000-tenant SaaS that would need Postgres sharding fits comfortably with one SQLite file per tenant on a few moderate VMs.
3. Edge deployments
Serving global users from one Postgres in us-east-1 means everyone outside the US pays 100ms of physics on every query. Cloudflare D1 + Turso lets you have data in every region. Reads hit local. Writes are propagated.
4. Local-first apps
Desktop and mobile apps that work offline and sync when online. The new wave (Linear, Figma’s local features, every modern Tauri app) builds on local SQLite plus a sync layer. Turso’s embedded replicas formalize this pattern.
5. Embedded analytical stuff
DuckDB if you want columnar OLAP. SQLite if you want OLTP-ish embedded. They’re complementary.
The Litestream pattern
For self-hosted SQLite, Litestream is the simplest path:
# litestream.yml
dbs:
- path: /data/app.db
replicas:
- type: s3
bucket: my-backups
path: app
region: ap-south-1
sync-interval: 1s
litestream replicate -config /etc/litestream.yml
Litestream tails the WAL and uploads changes to S3 (or any S3-compatible). Recovery:
litestream restore -o /data/app.db s3://my-backups/app
You get continuous backup, point-in-time restore, and an answer to “what if the disk dies.” For a single-node service that doesn’t need read replicas, Litestream is enough.
The Turso / libSQL pattern
libSQL is a fork of SQLite that adds:
- A server mode (HTTP and gRPC).
- Native replication.
- Embedded replicas that sync from a remote primary.
- Multi-database primitives for SaaS.
Turso is the managed offering. Self-hosted is also viable.
// embedded replica in Bun/Node
import { createClient } from "@libsql/client";
const db = createClient({
url: "file:local-replica.db",
syncUrl: "libsql://my-app.turso.io",
authToken: process.env.TURSO_AUTH,
syncInterval: 60, // sync every 60s
});
await db.sync();
const rows = await db.execute("SELECT * FROM users WHERE id = ?", [42]);
// ^^ reads from local file, ~600μs
The killer property: the same code works locally for dev, in CI for tests, and in prod. No different drivers, no embedded-vs-network split.
For Python:
import libsql_experimental as libsql
conn = libsql.connect(
"local-replica.db",
sync_url="libsql://my-app.turso.io",
auth_token=TOKEN,
)
conn.sync()
rows = conn.execute("SELECT * FROM users WHERE id = ?", (42,)).fetchall()
Cloudflare D1
D1 is SQLite as a service inside Cloudflare Workers:
export default {
async fetch(req: Request, env: { DB: D1Database }) {
const { results } = await env.DB
.prepare("SELECT id, email FROM users WHERE active = ?")
.bind(true)
.all();
return Response.json(results);
},
};
# wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "my-app"
database_id = "..."
Reads run in Cloudflare’s data center closest to the user. D1 transparently replicates writes globally. For a Workers-shaped app, this is the path of least resistance.
A real schema and migration
SQLite migrations are subtler than Postgres. The ALTER TABLE story is partial — many ALTERs require a copy-and-rename dance (handled for you by libraries like sqlite-utils or by Drizzle/Atlas). Plan migrations in advance.
-- A typical schema; works identically across SQLite, libSQL, D1
CREATE TABLE users (
id TEXT PRIMARY KEY, -- UUIDv7 stored as text
email TEXT NOT NULL,
full_name TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(email)
) WITHOUT ROWID, STRICT;
CREATE INDEX users_active ON users(is_active);
Three SQLite-specific keywords worth knowing:
WITHOUT ROWID— uses the primary key directly, no extra rowid; smaller and faster for primary-key-driven access.STRICT— enforces declared types. WithoutSTRICT, SQLite is type-flexible, which is friendly until it isn’t.INTEGERfor booleans — SQLite has noBOOLEAN; conventionally store 0/1.
Migrations: use Drizzle (TS), sqlx (Rust), Alembic with the SQLite dialect (Python), or Atlas (any). All handle the copy-and-rename for tricky ALTERs.
Performance numbers I’ve measured
A small benchmark on a c6i.large (2 vCPU, 4 GB) AWS EC2:
| Operation | Postgres (managed) | SQLite (local file) | SQLite (Turso embedded) |
|---|---|---|---|
| PK read p50 | 2.1 ms | 0.04 ms | 0.6 ms |
| PK read p99 | 8.2 ms | 0.18 ms | 1.4 ms |
| Range scan 1k rows | 12 ms | 1.5 ms | 2.1 ms |
| Insert (single) | 3 ms | 0.3 ms | 35 ms (network round-trip) |
| Insert batch of 1000 | 60 ms | 8 ms | 80 ms |
Reads obliterate. Single inserts pay the network tax (because the embedded replica writes through to primary). Batched writes are competitive. Plan accordingly.
Where SQLite stops being the answer
You’ll outgrow SQLite when one of these is true:
- Sustained writes >2,000/sec on a single database. SQLite serializes writes; one writer is the limit.
- Heavy long transactions across many tables. WAL mode helps, but contention on a single SQLite file rises fast.
- Row-level security required. Postgres RLS has no SQLite analog.
- Heavy use of Postgres extensions — pgvector, PostGIS, TimescaleDB. Some have SQLite analogs (
sqlite-vss,spatialite); most are weaker. - Data >100 GB. Operationally fine, but backup/restore time and corner cases multiply.
- Strong consistency across regions. SQLite (any flavor) is eventually consistent globally; if you need strict serializability across regions, look at Spanner / CockroachDB.
For transactional, high-write, complex-query workloads, Postgres still wins. The point of this post isn’t “ditch Postgres.” It’s “stop reaching for Postgres when SQLite would be simpler.”
A decision rule
Pick SQLite if:
- The app is read-heavy (>90% reads).
- You can tolerate single-region writes.
- Per-tenant isolation is a feature, not a hassle.
- You want the simplest possible operational story.
Pick Postgres if:
- You’ll have multi-writer concurrency at scale.
- You need extensions (pgvector for AI, PostGIS for geo, TimescaleDB for time series).
- You need RLS, sophisticated transactions, or strict cross-region consistency.
For most SaaS apps starting in 2026, SQLite + Turso is the right default, and you migrate to Postgres only if you need to. Reverse what your instincts told you in 2022.
Production checklist
- WAL mode (
PRAGMA journal_mode = WAL). -
synchronous = NORMALfor the right durability/perf tradeoff. -
foreign_keys = ON(off by default — this surprises people). - Bound
cache_sizeandtemp_store = MEMORYfor hot performance. - Backup strategy (Litestream, Turso continuous backup, or
VACUUM INTO). - Migration tool (Drizzle / sqlx / Alembic / Atlas).
- STRICT tables for new schemas.
- Monitoring (per-DB sizes, WAL size, vacuum frequency).
- A plan for “what if writes >2k/sec” before you hit it.
Read this next
- PostgreSQL 18 — What’s New — the alternative.
- Distributed Systems Fundamentals — the consistency model conversation.
- Caching Strategies in 2026 — what SQLite-as-cache patterns look like.
- Modern TypeScript Backend with Hono on Bun — pairs naturally with Turso for the JS-first edge stack.
If you want a working “Hono + Turso embedded replica + Drizzle” SaaS starter that runs the same code on your laptop, in CI, and on Fly/Render/Workers, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .