Your SQLite Problem Just Got More Interesting
You’ve been running SQLite locally, it works great, and then someone asks: “But what if the server dies?” You smile, nod, and realize you need to think about this differently.
Here’s the thing: SQLite is fantastic for single-server setups. It’s fast, zero configuration, no daemons, and fits in a pocket watch. But it’s not built for clustering. You can’t just point three instances at the same file and expect consensus.
Enter two very different philosophies: Litestream treats SQLite as a fire-and-forget application, continuously streaming changes to cloud storage. rqlite wraps SQLite with Raft consensus, turning it into a real distributed database. Both solve the durability problem. Neither tries to be Postgres.
The choice isn’t “which is better”—it’s “which fits your failure tolerance and operational complexity budget?”
Litestream: Continuous Backup as Your Replication Strategy
Litestream runs alongside your SQLite instance and streams Write-Ahead Log (WAL) frames to cloud storage—S3, Backblaze B2, or any S3-compatible target. Think of it as a live backup that never stops running.
How it works:
Your app writes to app.db. SQLite creates a WAL file (app.db-wal) that holds uncommitted changes. Litestream watches that WAL, batches up frames, and pushes them to S3 every few seconds (configurable). When disaster strikes, you restore from S3 and you’re back where you left off—no data loss, because the WAL is already on cloud storage.
Setup example:
dbs: - path: /data/app.db replicas: - url: s3://my-bucket/db-backups/app.db access-key-id: ${LITESTREAM_AWS_ACCESS_KEY_ID} secret-access-key: ${LITESTREAM_AWS_SECRET_ACCESS_KEY} region: us-east-1 retention: 720h # 30 days sync-interval: 5sDrop this in your app directory, set your S3 credentials, and run:
litestream replicateThat’s it. Litestream forks into the background, starts watching your database, and streams WAL frames. Your app doesn’t know it’s happening. No connection pooling, no consensus overhead, no raft elections.
The restore drill:
Your server catches fire. You spin up a new one, grab the latest backup from S3:
litestream restore -o /data/app.db s3://my-bucket/db-backups/app.dbLitestream fetches the most recent snapshot and replays the WAL up to the last frame it pushed. You’re live again. This takes seconds on a multi-gigabyte database because you’re not waiting for a full copy—you’re replaying the transaction log.
Real trade-off: Litestream only helps if your database file survives (or you have a recent snapshot). If the VM itself dies mid-write and you lose the WAL, you’re back to your last S3 sync interval. With sync-interval: 5s, you lose at most 5 seconds of committed work. For most self-hosted projects, that’s acceptable. For financial transactions or medical records, you’d want synchronous durability (which Litestream doesn’t offer).
Also: you’re not getting read scaling. Backups live on S3. If you need to read from a replica, you need a separate replica database (which Litestream can restore to, but you manage the failover).
rqlite: Raft Consensus with SQLite as the Engine
rqlite is a distributed SQL database built on SQLite. It clusters nodes using Raft consensus—the same algorithm used by Consul, etcd, and Kubernetes. Every write is replicated across the cluster before it’s committed to the log. Reads can happen locally or on any replica.
How it works:
You run three (or five, or seven) rqlite nodes. They elect a leader. When you write to the leader, it replicates the query to the followers, waits for a quorum to acknowledge, then commits. Reads are fast—they hit the local SQLite instance on any node.
Cluster setup example:
Node 1 (leader):
rqlited -http-addr=0.0.0.0:4001 \ -raft-addr=0.0.0.0:4002 \ -node-id=node1 \ /data/node1Node 2 (follower):
rqlited -http-addr=0.0.0.0:4001 \ -raft-addr=0.0.0.0:4002 \ -node-id=node2 \ -join=http://node1:4001 \ /data/node2Node 3 (follower):
rqlited -http-addr=0.0.0.0:4001 \ -raft-addr=0.0.0.0:4002 \ -node-id=node3 \ -join=http://node1:4001 \ /data/node3Now you have a three-node cluster. Writes go to the leader. Raft handles failure detection and leader re-election. If the leader dies, a new one is elected in seconds. Your app just points to any node; rqlite handles redirect or gossip internally (depending on your client).
Writing data:
curl -X POST http://localhost:4001/db/execute \ -d '{"statements": ["CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"]}'
curl -X POST http://localhost:4001/db/execute \ -d '{"statements": ["INSERT INTO users (name) VALUES ('"'"'Alice'"'"')"]}'Reading data:
curl http://localhost:4001/db/query?q=SELECT%20*%20FROM%20usersThe restore drill:
A node dies. rqlite automatically evicts it from the cluster. The remaining two nodes maintain quorum and keep running. When you bring the node back up and rejoin it (using -join=), it catches up via Raft snapshot + log replay. No manual restore script needed.
Real trade-off: Raft adds consensus overhead. Writes are slower than single-instance SQLite because you’re waiting for quorum. With three nodes, you need 2/3 to agree. That’s extra network round-trips. For a self-hosted app serving dozens of users, it’s fine. For a high-frequency trading system, you’d notice. Also, rqlite is less battle-tested in production than Postgres, and the community is smaller—you’re debugging with fewer StackOverflow answers.
The Decision Tree
Use Litestream if:
- You’re running a single SQLite instance and need durability without operational pain.
- You can tolerate 5–30 seconds of data loss (whatever your sync interval is).
- You want zero changes to your app code. Litestream is a sidecar; your app doesn’t care it exists.
- You’re backing up to S3/B2/cloud and that’s good enough for your RTO.
- You want instant failover (spin up a new VM, restore, run app).
- You don’t need read scaling across replicas.
Use rqlite if:
- You need automatic failover with zero human intervention.
- You can tolerate ~5–10ms of extra latency per write (Raft round-trip).
- You want read scaling across the cluster.
- You’re comfortable running 3+ nodes (adding infrastructure).
- Your failure domain is a single node, not an entire data center.
- You want a SQL interface without learning a distributed KV API.
Use both if:
- You’re running rqlite and want continuous backups to S3 for disaster recovery (meteor strike scenarios).
- You’re running Litestream but need immediate read replicas for reporting (Litestream + separate restored copies).
Real-World Example: Self-Hosted Wiki
Say you’re running Wiki.js with SQLite as the backend. Today, it lives on one server. You want high availability without running Postgres.
Option A: Litestream
Add litestream.yml, point it at a Backblaze B2 bucket (cheaper than S3), and run Litestream as a service alongside Wiki.js. If the server dies, you restore from B2 to a new VM and restart Wiki.js. Total RTO: 5 minutes (EC2 launch + restore + app boot). RPO: 5 seconds. Cost: B2 storage ($0.006/GB/month) + bandwidth.
Option B: rqlite
Run three rqlite nodes across three cheap VPSes. Point Wiki.js at the rqlite leader. If one node dies, the other two keep serving. No restore script, no S3 credentials, no manual failover. Cost: three VPSes instead of one, but you’re paying for availability uptime, not downtime recovery.
For a self-hosted wiki you’re not monetizing, Option A is faster to deploy and cheaper. For a wiki you’re running for a team and downtime is annoying, Option B buys you peace of mind.
The Catch: Schema Changes and Snapshots
Both tools struggle with schema migrations in one way:
Litestream: Your app handles schema changes (ALTER TABLE, etc.). Litestream replicates the WAL, not the schema. If you’ve got a rolling deploy (old and new versions running), make sure your schema changes are backward compatible. Litestream doesn’t care—it just streams bytes.
rqlite: Same story. Schema changes run on the leader, replicate via Raft to followers. All nodes converge on the same schema. But if you’re upgrading your app in a rolling manner, coordinate carefully.
Neither tool is smarter than your app logic here. Don’t expect replication to save you from a bad migration.
Operational Simplicity vs. Resilience
Here’s what I keep coming back to: Litestream is operational simplicity in a tarball. rqlite is resilience with a price tag.
Litestream is a single binary that watches your database and uploads. It fails loudly, restarts easily, and doesn’t require you to think about consensus. Your single-node application stays single-node. Everything you know about SQLite still works. You just trade some durability guarantees for simplicity.
rqlite flips the trade: you get automatic failover, read scaling, and true distributed durability. But you’re now running a cluster. You need monitoring, health checks, and a way to handle split-brain (though Raft helps here). You’re managing more infrastructure.
For a homelab project you’re tinkering with on weekends, Litestream wins. For a production service where uptime matters and you’ve got the infrastructure to run multiple nodes anyway, rqlite is worth considering.
Getting Started
Litestream:
# Installcurl https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.tar.gz | tar xzsudo mv litestream /usr/local/bin/
# Create configcat > /etc/litestream.yml <<EOFdbs: - path: /data/app.db replicas: - url: s3://bucket/app.db access-key-id: YOUR_KEY secret-access-key: YOUR_SECRETEOF
# Runlitestream replicate -config /etc/litestream.ymlrqlite:
# Installcurl https://github.com/rqlite/rqlite/releases/download/v8.24.0/rqlite-v8.24.0-linux-amd64.tar.gz | tar xzsudo mv rqlite /usr/local/bin/
# Start clusterrqlited -node-id=node1 /data/node1 &Then join your other nodes and you’re live.
Both solve the “what if the server dies” problem. Litestream is a lifeguard watching the pool. rqlite is a safety net spread across three pools. Pick based on what you’re willing to operate, not what sounds fancier.