Running SQLite in Production with Litestream

Photo by Forest Wander from Cross Lanes via Wikimedia Commons (CC BY-SA 2.0)
SQLite is production-ready for the right shape of workload: read-heavy, single-node applications, roughly under ten thousand daily active users. The main gap has always been durability on a single disk, and Litestream closes it by streaming changes to object storage. For many small services it is faster and cheaper than a managed database.
It runs as a background process, controls WAL checkpointing, and captures new database pages as your app commits writes. It periodically takes a full snapshot and streams the incremental changes between snapshots up to object storage like S3, R2, or B2. To restore, it downloads a snapshot and replays the changes recorded after it.
Yes. The restore command accepts a timestamp and rebuilds the database as it existed at that exact moment, as long as it falls inside your configured retention window. This is the recovery path for a bad migration, a botched bulk update, or an accidental delete. Without a timestamp you get the latest replicated state.
The 0.5.0 release replaced WAL segments with a transaction-aware LTX format and added hierarchical compaction across 30-second, 5-minute, and hourly windows, so restores touch about a dozen files instead of thousands. It also removed the generations concept in favor of monotonic transaction IDs and dropped the CGO dependency by moving to a pure-Go SQLite build.
No. Litestream is disaster recovery, not high availability. Replication is asynchronous, so a disk failure can lose the last second or so of writes, and version 0.5.0 allows only one replica destination per database. If you need zero data loss, automatic failover, or many concurrent writers, use Postgres instead.

Photo by Forest Wander from Cross Lanes via Wikimedia Commons (CC BY-SA 2.0)
Key Takeaway
Litestream is a background process that streams a SQLite database's write-ahead log to object storage like S3, giving you continuous backup and point-in-time restore without a database server. For read-heavy, single-node apps under roughly ten thousand daily users, it turns a plain SQLite file into a durable, recoverable production store.
For years the reflex was automatic: production means Postgres, and SQLite is for tests and prototypes. I stopped believing that after running a few small services where a single SQLite file on a VPS was faster, cheaper, and simpler than a managed database I was paying for by the hour. The one thing that always held me back was durability. A single file on one disk is one bad volume away from gone. Litestream is the tool that closes that gap, and its 0.5.0 release in late 2025 made it materially better.
This is a practical walkthrough of how Litestream works, how to wire it up next to a real app, how point-in-time restore behaves when things go wrong, and — just as important — the workloads where I would still reach for Postgres instead. No magic, just a background process copying the right bytes to the right bucket.
Litestream runs as a separate process next to your application. It opens a long-lived read connection to your SQLite database so that WAL checkpointing is controlled by Litestream rather than happening randomly. As your app commits writes, SQLite appends them to the write-ahead log; Litestream watches that log, captures the new pages, and copies them up to object storage on a short interval. It periodically takes a full snapshot and then streams the incremental changes between snapshots. To rebuild the database anywhere, you download a snapshot and replay the changes recorded after it.
Version 0.5.0 replaced the old WAL-segment model with a transaction-aware format called LTX and added hierarchical compaction. Small change files from 30-second windows get compacted into 5-minute windows, and those into hourly windows. The practical payoff is that a restore only needs to touch about a dozen files on average instead of replaying thousands of tiny segments — restores that used to feel slow now finish quickly. The release also dropped the confusing generations concept in favor of monotonically incrementing transaction IDs, and removed the CGO dependency by moving to a pure-Go SQLite implementation, which makes the binary trivial to drop into a scratch Docker image.
The whole setup is one YAML file that lists each database and where its replica lives. Point it at any S3-compatible endpoint — real AWS, Cloudflare R2, Backblaze B2, or a MinIO bucket you host yourself. I run most of mine against R2 because egress is free, which matters when a restore pulls the whole dataset down.
# /etc/litestream.yml
access-key-id: ${LITESTREAM_ACCESS_KEY_ID}
secret-access-key: ${LITESTREAM_SECRET_ACCESS_KEY}
dbs:
- path: /data/app.db
replicas:
- type: s3
bucket: my-app-backups
path: app.db
endpoint: https://<account>.r2.cloudflarestorage.com
region: auto
# snapshot on a schedule so restores stay fast
snapshot-interval: 6h
# keep 30 days of history for point-in-time restore
retention: 720hYou start replication with a single long-running command. In production I let systemd or Docker supervise it so it restarts if it ever dies.
# run as a supervised service
litestream replicate -config /etc/litestream.yml
# verify what has actually been replicated
litestream snapshots /data/app.db
litestream wal /data/app.dbPut your database file on a path that is NOT wiped on redeploy, and give Litestream a moment to finish its first snapshot before you send production traffic. Run the snapshots command in your deploy check — if it returns nothing, replication is not actually happening and you have a silent gap in your backups.
The reason you run Litestream is the day you need to undo something. Restore reads a snapshot plus the changes after it and rebuilds the file. Without a timestamp you get the latest state; with a timestamp you get the database as it existed at that exact moment, as long as it falls inside your retention window. This is the escape hatch for a bad migration, a botched bulk update, or a delete-without-a-where-clause at three in the morning.
# restore the latest state to a fresh path
litestream restore -o /data/app-restored.db /data/app.db
# restore to a specific point in time (UTC, RFC3339)
litestream restore \
-timestamp 2026-07-10T14:30:00Z \
-o /data/app-recovered.db \
/data/app.db
# bootstrap on a brand-new server straight from the bucket
litestream restore -config /etc/litestream.yml /data/app.dbThat last form is the quiet superpower. On a fresh VPS with an empty data volume, Litestream restores the database directly from object storage before the app starts. Your deploy becomes: pull the binary, restore from the bucket, start replicating, start the app. There is no separate backup-restore runbook because restore is the bootstrap.
Litestream is disaster recovery, not high availability. Replication is asynchronous, so if the disk dies you can lose the last second or so of writes that had not shipped yet. And 0.5.0 enforces one replica destination per database — it is a backup pipeline, not a live cluster with automatic failover. If you need zero data loss or instant failover, this is the wrong tool.
With durability solved, the case for SQLite gets strong for a specific shape of workload. The database is a file in the same process as your app, so reads are a function call, not a network round trip. There is no connection pool to tune, no separate server to patch, and no per-hour bill. In WAL mode readers never block the writer and the writer never blocks readers.
The hard limit is the single writer. SQLite serializes writes, so a workload with many independent processes hammering writes concurrently — background workers, high-fan-out ingestion, hundreds of simultaneous writers — is exactly what Postgres and its row-level MVCC locking exist for. On modern NVMe a single SQLite writer still pushes surprising throughput, but if your bottleneck is write concurrency rather than write volume, do not fight the model. Pick the database that matches the shape of your writes.
| Concern | SQLite + Litestream | Managed Postgres |
|---|---|---|
| Concurrent writers | One at a time (serialized) | Many, row-level MVCC |
| Read latency | In-process, no network hop | Network round trip per query |
| Durability model | Async replication to object storage | Synchronous WAL + replicas |
| Failover | Manual restore (DR, not HA) | Automatic with a standby |
| Operational cost | One binary + a bucket | Managed service billed hourly |
On my own VPS I run Litestream as a second process in the same Docker Compose stack as the app, sharing the data volume. The container entrypoint restores from the bucket if the file is missing, then starts replicate. I keep a 30-day retention and a six-hour snapshot interval, which balances restore speed against storage cost, and I test a restore into a throwaway path on a schedule — a backup you have never restored is a rumor, not a backup. That single loop has turned SQLite from a prototyping convenience into something I am comfortable running real traffic on.