Skip to main content
Can You Run SQLite in Production? (2026)

Can You Run SQLite in Production? (2026)

KU
• 7 min read•Updated •68 views

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.

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:

SQL
-- 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:

code
$ 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.

YAML
# /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:

code
$ 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 textCauseFix
database is locked (SQLITE_BUSY, code 5)Another connection holds the write lockSet 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 committedStart 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 readingFinish or reset the open statement first
-wal file grows without boundCheckpoint starvation: overlapping readers never let the WAL resetLeave read gaps, or run PRAGMA wal_checkpoint(TRUNCATE);
unable to open database file in a containerDirectory not writable, so -wal and -shm cannot be createdMount 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

Explore topics:Cloud
React
Share this post
Cite this article
Plain text

Kiril Urbonas (2026). Can You Run SQLite in Production? (2026). DevOpsNess. Updated 2026-10-31. https://www.devopsness.com/blog/sqlite-in-production

Markdown

[Can You Run SQLite in Production? (2026)](https://www.devopsness.com/blog/sqlite-in-production) (Kiril Urbonas, DevOpsNess, updated 2026-10-31)

KU

Kiril Urbonas

AI Engineer

585 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.