IAnswerable

When SQLite Is Fine in Production

30 Jan 2026

SQLite has a reputation as a toy database. It isn’t. A single SQLite file in WAL mode on decent disk handles far more load than most side projects ever see, and it removes an entire service from your stack. But it has a real ceiling, and hitting it in production is a bad afternoon.

Where SQLite is fine

Single-writer workloads. SQLite allows one writer at a time, but unlimited concurrent readers in WAL mode. If your app is read-heavy — a blog, a docs site, an internal dashboard, most CRUD apps under real-world traffic — one writer is not the bottleneck you think it is. Writes are fast; they just serialize.

Single-instance deploys. If you run one app server (common for small products and internal tools), there’s no multi-writer contention across machines to worry about, because there’s only one machine touching the file.

Edge and embedded. Cloudflare D1, Turso, and similar are SQLite under the hood specifically because it embeds well and doesn’t need a network round trip to a separate database process.

Read-mostly APIs, caches, and local-first apps. Anywhere the data fits on one disk and doesn’t need to be queried from five different app servers simultaneously.

Where it will hurt you

Multiple app instances writing to the same file over a network filesystem. NFS and SQLite locking do not get along — you can get corruption, not just contention. If you’re horizontally scaling app servers and they all need write access, that’s a real database, not SQLite on a shared volume.

High write concurrency. If you have genuinely concurrent writers — many users writing at once, background jobs writing continuously while the API also writes — the single-writer lock becomes a queue, and under load that queue backs up into request latency.

You need replication or built-in HA. SQLite has none of this natively. Litestream and similar tools give you continuous backup and disaster recovery, but not multi-master write availability.

Setting it up correctly

Default SQLite settings are wrong for production. Fix these:

PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA busy_timeout = 5000;

Without busy_timeout set, you will see database is locked errors under any concurrent write load, even light load, because the default is to fail immediately instead of waiting.

Backing it up

A single file is trivially easy to back up — copy it. But copying a live SQLite file with plain cp while it’s being written can grab it mid-transaction. Use the backup API instead:

sqlite3 app.db ".backup /backups/app-$(date +%F).db"

Or run Litestream for continuous streaming replication to S3-compatible storage — it tails the WAL and ships changes continuously, giving you point-in-time recovery without a separate database server.

The decision

Signal Verdict
One app server, read-heavy SQLite is fine
Multiple app servers, need shared writes Use Postgres
Need replicas / read scaling across regions Turso, or Postgres with read replicas
Occasional background writer + mostly reads SQLite is fine
High-concurrency writes from many sources Postgres

If you’re not sure which bucket you’re in, you’re probably still in the “SQLite is fine” bucket — the workloads that actually need Postgres’s concurrency model are usually obvious, not marginal.