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

ComponentJob
SQLiteThe engine itself
WAL modeConcurrent readers + one writer; crash safety
LitestreamContinuous replication of the WAL to S3-compatible storage
libSQL / TursoSQLite fork with HTTP server, embedded replicas, multi-tenant primitives
Cloudflare D1Managed SQLite at the edge, integrated with Workers
LiteFS (Fly.io)Distributed SQLite as a virtual filesystem
rusqlite, better-sqlite3, sqlx-sqliteNative 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. Without STRICT, SQLite is type-flexible, which is friendly until it isn’t.
  • INTEGER for booleans — SQLite has no BOOLEAN; 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:

OperationPostgres (managed)SQLite (local file)SQLite (Turso embedded)
PK read p502.1 ms0.04 ms0.6 ms
PK read p998.2 ms0.18 ms1.4 ms
Range scan 1k rows12 ms1.5 ms2.1 ms
Insert (single)3 ms0.3 ms35 ms (network round-trip)
Insert batch of 100060 ms8 ms80 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 = NORMAL for the right durability/perf tradeoff.
  • foreign_keys = ON (off by default — this surprises people).
  • Bound cache_size and temp_store = MEMORY for 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

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 .