A practical look at when SQLite is a genuinely good production database, where it breaks, and the tooling that makes it viable.
For years the reflex answer was no. SQLite was the thing that stored your phone's contacts or your browser history, a file format pretending to be a database, fine for a unit test but not for anything real. If you were building a service, you reached for Postgres or MySQL without thinking about it.
That reflex is now wrong more often than it's right, and it's worth understanding why.
Start with scale. SQLite is almost certainly the most-deployed database engine on the planet. It ships in every browser, every phone, most cars, and countless embedded devices. It is boring, battle-tested code that has been fuzzed and audited to an extreme degree. "Nobody got fired for choosing Postgres" has a quiet cousin: nobody has ever found SQLite to be the flaky part of their stack.
Then there's raw speed. Because the database lives in the same process as your application, a read is a function call, not a network round trip. There is no connection pool, no wire protocol, no separate server to saturate. For read-heavy work, a well-indexed SQLite query returns before a networked database has finished its TCP handshake.
The thing that actually revived it, though, is the edge and serverless wave. When your compute runs in dozens of locations close to users, a single central Postgres instance becomes the slow part. Suddenly a database that lives as a file next to your code, replicable to every region, is not a compromise. It's the point.
The workloads where SQLite shines are more common than people assume:
The common thread is valuing simplicity. No separate database process means nothing to provision, patch, secure, or wake up for at 3 a.m.
Be honest about the constraint: SQLite has a single writer. Writes serialize. Only one write transaction can be in flight at a time for a given database file. There is also no built-in network access or replication. The database is a file on a disk, full stop.
Those two facts are the whole story of "SQLite in production," and both are addressable.
The first fix is Write-Ahead Logging. In the default rollback-journal mode, a writer blocks readers. WAL mode changes that so readers and one writer proceed concurrently, which is exactly what a web app wants. Turn it on once and it sticks with the database file:
-- Run once per database (WAL is persistent across connections)
PRAGMA journal_mode = WAL;
-- Set on every connection your app opens
PRAGMA busy_timeout = 5000; -- wait up to 5s for a lock instead of erroring
PRAGMA synchronous = NORMAL; -- safe with WAL, much faster than FULL
PRAGMA foreign_keys = ON;
PRAGMA cache_size = -20000; -- ~20MB page cache (negative = KiB)
The busy_timeout is the setting people forget. Without it, a write that hits a momentary lock fails immediately with SQLITE_BUSY. With it, the connection waits and retries for you, which turns most concurrency problems into a few milliseconds of latency instead of an error. Because writes serialize, funnel them through a single connection or a small serialized pool rather than a large one.
The second fix is replication and durability, and this is where the modern tooling lives:
Litestream is the one I reach for first because it solves the scariest part (losing data on a single node) with a tiny config:
# /etc/litestream.yml
dbs:
- path: /var/lib/app/data.db
replicas:
- type: s3
bucket: my-app-backups
path: prod/data.db
region: us-east-1
# Snapshot periodically, stream WAL continuously
sync-interval: 1s
retention: 72h
Run litestream replicate alongside your app and restore with litestream restore on a fresh box. If you need multi-region reads, LiteFS layers on top, electing a primary for writes and serving reads locally everywhere else.
The honest limits, stated plainly:
LISTEN/NOTIFY, advanced full-text and vector extensions, stored procedures, fine-grained roles. If your design leans on those, use the database that has them.Reach for SQLite when reads dominate, when your writes fit comfortably through one serialized channel, when you value one fewer moving part, or when you're deploying to the edge and latency to a central database is the enemy. Per-tenant SaaS, content-driven sites, internal tools, and embedded apps all land squarely in that zone. For the broader picture of where this fits, see our serverless and edge databases guide and the deeper dive on edge databases for low-latency apps.
Default to SQLite for new read-heavy or single-node services, and don't apologize for it. Turn on WAL, set busy_timeout, serialize your writes, and put Litestream in front of it on day one so a dead disk is a non-event. Reach for Turso or LiteFS the moment you need reads in multiple regions. Only graduate to Postgres when you hit a real write-concurrency wall or need a feature SQLite doesn't have, and by then you'll know exactly why you're paying for the extra tier.
The old assumption was that SQLite couldn't do production. The current reality is that for a large slice of applications, it's the simpler and faster choice, and the tooling has closed the gap on everything else.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Explore more articles in this category
A hands-on walkthrough of Turso and libSQL, from CLI setup to embedded replicas that put SQLite reads next to your users.
A practitioner's guide to choosing between serverless and provisioned databases based on cost, latency, connections, and load shape.
The database is going serverless and moving to the edge. This is the map: what these terms actually mean, when they win, and how the options compare.
Evergreen posts worth revisiting.