Can You Run SQLite in Production? (2026)
A practical look at when SQLite is a genuinely good production database, where it breaks, and the tooling that makes it viable.
Key takeaways
- Yes.
- SQLite runs well in production for read-heavy, single-node and per-tenant workloads, provided you enable WAL mode, set busy_timeout, serialize writes and stream backups off the box with Litestream.
- Its limits are one writer at a time and no network access, so heavy concurrent writes or many servers sharing one database need Postgres.
On this page
Yes, for read-heavy, single-node and per-tenant workloads. Turn on WAL mode, set busy_timeout, send writes through one connection, and stream backups to object storage with Litestream. The real limits are one writer at a time and no network access, so heavy concurrent writes or several app servers sharing one database still belong on Postgres.
Verified against
SQLite 3.54.1, Litestream 0.5.x config format, LiteFS (pre-1.0), Cloudflare D1, against the sqlite.org documentation, litestream.io reference, docs.fly.io and Cloudflare D1 limits pages. Checked October 2026.
For years the reflex answer was no: SQLite stored your phone's contacts, fine for a unit test but not for a service. That reflex is now wrong more often than it is right.
Why the answer changed
SQLite's own site calls it the most widely deployed database engine, with "billions and billions of copies" in phones, browsers, TVs and cars. It is boring, heavily tested code. Because the database lives in your process, a read is a function call, not a network round trip: no pool, no wire protocol, no separate server to saturate.
The sqlite.org "When to use" page is unusually specific: sites with fewer than 100K hits a day should work fine, that figure is "a conservative estimate, not a hard upper bound", and SQLite has been shown to handle 10 times that. The edge wave did the rest. When compute runs in many regions, a central Postgres becomes the slow part, and a database that is a file next to your code becomes the point.
Where it fits
- Read-heavy apps: docs sites, blogs, dashboards, catalogs.
- Single-node services: if the app runs on one box, a local database removes an entire tier.
- Per-tenant databases: one file per customer. Isolation is physical and backups are per tenant.
- Embedded and edge: CLIs, desktop apps, IoT, edge functions.
- Low-to-moderate write concurrency: internal tools and small SaaS. Most apps do not have thousands of concurrent writers.
The production config, copy and paste
SQLite has a single writer per database file. WAL mode lets readers and that one writer proceed concurrently, and the setting persists in the file. The other pragmas are per connection, so set them every time your app opens one:
-- once per database file (persistent)
PRAGMA journal_mode = WAL;
-- every connection
PRAGMA busy_timeout = 5000; -- wait up to 5 s for a lock instead of failing with SQLITE_BUSY
PRAGMA synchronous = NORMAL; -- fast with WAL; a power loss can roll back the last commits
PRAGMA foreign_keys = ON; -- off by default
PRAGMA cache_size = -20000; -- about 20 MB page cache (negative means KiB)
Be honest about synchronous = NORMAL: the WAL docs say it can lose recently committed transactions on power loss or a hard reset. The database stays consistent; the last few writes may vanish. If that is unacceptable, keep FULL.
Write transactions should start with BEGIN IMMEDIATE. The result-code docs explain why: a deferred transaction that reads and then writes can hit SQLITE_BUSY mid-transaction, while one that starts IMMEDIATE either gets the write lock up front or fails before doing any work.
Check what you actually got:
$ sqlite3 /var/lib/app/data.db 'PRAGMA journal_mode;'
wal
$ ls /var/lib/app/
data.db data.db-shm data.db-wal
Backups: Litestream on day one
Litestream streams WAL changes to S3-compatible storage continuously. The config format changed in 0.5.0: each database now has a single replica object, and the old replicas list is deprecated. Snapshot and retention settings are global.
# /etc/litestream.yml
dbs:
- path: /var/lib/app/data.db
replica:
type: s3
bucket: my-app-backups
path: prod/data.db
region: us-east-1
sync-interval: 1s
snapshot:
interval: 24h
retention: 72h
Run litestream replicate as a sidecar or systemd unit next to the app. On a fresh box, restore before the app starts; this form is idempotent and safe in an entrypoint:
$ litestream restore -if-db-not-exists -if-replica-exists /var/lib/app/data.db
$ litestream restore -o /tmp/restore-test.db -timestamp 2026-10-30T09:00:00Z /var/lib/app/data.db
Restore to a scratch path on a schedule and run PRAGMA integrity_check; against it. A backup you have never restored is a hope, not a backup.
Replication and managed options
- LiteFS replicates a SQLite database across nodes through a FUSE filesystem, with one primary taking writes. As of October 2026, Fly's docs call it stable and in production use but still pre-1.0, and "outside our scope of support". They also warn against running it with Fly Machine autostop, which can lose data.
- Turso offers a managed platform for libSQL and its newer Turso Database engine; the Turso tutorial walks through it.
- Cloudflare D1 is managed SQLite inside Workers. Its limits page lists 10 GB per database on the paid plan, and each database processes queries one at a time, so at 1 ms per query you get about 1,000 queries per second. Our Cloudflare D1 guide covers the design that implies.
The errors you will see, and the fix
| Error text | Cause | Fix |
|---|---|---|
database is locked (SQLITE_BUSY, code 5) | Another connection holds the write lock | Set busy_timeout, use BEGIN IMMEDIATE, funnel writes through one connection |
SQLITE_BUSY_SNAPSHOT (517) | In WAL, a read transaction tried to upgrade to write after another writer committed | Start write transactions with BEGIN IMMEDIATE |
database table is locked (SQLITE_LOCKED, 6) | Conflict inside the same connection, e.g. dropping a table you are still reading | Finish or reset the open statement first |
-wal file grows without bound | Checkpoint starvation: overlapping readers never let the WAL reset | Leave read gaps, or run PRAGMA wal_checkpoint(TRUNCATE); |
unable to open database file in a container | Directory not writable, so -wal and -shm cannot be created | Mount the whole directory read-write, not only the file |
Where this breaks
- Several app servers, one file. WAL needs shared memory on one host, and the docs warn that file locking over network filesystems is often buggy and can corrupt data. Never put the database on NFS or EFS for multi-host writes.
- High write concurrency. Writes serialize. Per-tenant sharding pushes the ceiling out, but a true multi-writer OLTP system wants Postgres.
- Postgres-specific features.
LISTEN/NOTIFY, rich types, row-level security, fine-grained roles. If your design leans on those, use the database that has them, and weigh the serverless options in serverless vs traditional databases.
Questions people ask
Is SQLite good enough for a production website?
For most sites, yes. The sqlite.org guidance says any site with fewer than 100K hits a day should work fine, calls that a conservative estimate, and notes SQLite has handled ten times that. The constraint is writes, not reads: one writer at a time. Use WAL mode, busy_timeout, and Litestream backups, and keep the app on a single server.
How do I fix "database is locked" in SQLite?
It is SQLITE_BUSY: another connection holds the write lock. Set PRAGMA busy_timeout = 5000 on every connection so SQLite waits instead of failing, enable WAL so readers stop blocking the writer, and begin write transactions with BEGIN IMMEDIATE. If it persists, look for a long-running write transaction or many writer connections, and route writes through one connection.
Can multiple servers share one SQLite database?
Not safely over a network filesystem. WAL mode requires every process to be on the same host because it uses shared memory, and SQLite's docs warn that network filesystem locking is often buggy and can corrupt the database. For multiple servers, use LiteFS-style replication with a single write primary, a managed service such as Turso or D1, or move to Postgres.
The call we'd make
Default to SQLite for new read-heavy or single-node services, and do not apologize for it. Turn on WAL, set busy_timeout, use BEGIN IMMEDIATE for writes, and run Litestream from the first deploy so a dead disk is a non-event. Reach for Turso, D1 or LiteFS when you need reads in many regions. Graduate to Postgres when you hit a real write-concurrency wall or need a feature SQLite lacks; for the wider set of options, see the serverless and edge databases guide and edge databases for low-latency apps.
Sources
Cite this article
Kiril Urbonas (2026). Can You Run SQLite in Production? (2026). DevOpsNess. Updated 2026-10-31. https://www.devopsness.com/blog/sqlite-in-production
[Can You Run SQLite in Production? (2026)](https://www.devopsness.com/blog/sqlite-in-production) (Kiril Urbonas, DevOpsNess, updated 2026-10-31)
More from Cloud
Explore more articles in this category
Kubernetes 1.34 Hit End of Life Oct 27: What EKS, GKE and AKS Do Next
Upstream stopped patching 1.34 on October 27. EKS starts billing 6x on December 2, AKS drops it at the end of November, and GKE gives you until January. The dates, the commands to find your 1.34 clusters, and the upgrade order.
.NET 8 Support Ends November 10: The Lambda and Azure Functions Checklist
.NET 8, .NET 9, the Lambda dotnet8 runtime and the Azure Functions in-process model all lose support on November 10, 2026. Inventory commands, the isolated-worker migration and a CI gate.
Lambda Python 3.10 Is Deprecated Today: Find Every Function and Move to 3.13
AWS deprecated the Lambda python3.10 runtime on October 31, 2026. Functions keep running, patches stop. The inventory commands, IaC diffs and AL2023 gotchas for moving to python3.13.
You might have missed
Evergreen posts worth revisiting.