The architecture
- FUSE filesystem at the path your app's SQLite file lives. The app sees a normal file; behind the scenes LiteFS intercepts writes.
- Primary node — the writer. Selected by lease-based leader election (via Consul, or a static config).
- Replica nodes — followers. Receive WAL frames from the primary; apply them to their local SQLite file; serve reads locally.
- Replication lag — sub-second typical on a normal LAN; multi-second across continents.
Install
LFS_VER=v0.5.13
curl -L "https://github.com/superfly/litefs/releases/download/${LFS_VER}/litefs-linux-amd64.tar.gz" \
| tar -xz
sudo install litefs /usr/local/bin/
# Need FUSE on the host
sudo apt install fuse3
The basic setup (Consul-backed lease)
LiteFS needs a lease mechanism to know who's the writer. Easiest is Consul (or use static for single-region testing):
# /etc/litefs.yml on each node
fuse:
dir: "/var/lib/litefs" # FUSE mount point; this is where SQLite files go
allow-other: true
data:
dir: "/var/lib/litefs/data" # raw replication data
proxy:
addr: ":20202" # if your app talks to a port; LiteFS can proxy to redirect writes
target: "localhost:8080"
db: "myapp.db"
lease:
type: "consul"
hostname: "node-1.lab" # how other nodes contact this one
advertise-url: "http://node-1.lab:20202"
consul:
url: "http://consul:8500"
key: "litefs/myapp/lease"
http:
addr: ":20203"
On each node, the LiteFS daemon starts; one wins the lease (becomes primary), others become replicas.
Run it
sudo litefs mount -config /etc/litefs.yml
# Or as a systemd service
sudo tee /etc/systemd/system/litefs.service <<'EOF'
[Unit]
Description=LiteFS
After=network-online.target
[Service]
ExecStart=/usr/local/bin/litefs mount -config /etc/litefs.yml
Restart=always
ExecStop=/bin/fusermount3 -u /var/lib/litefs
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now litefs
Point your app at the mounted path
The SQLite database file lives at /var/lib/litefs/myapp.db. Your app opens it normally:
// Go
db, err := sql.Open("sqlite3", "/var/lib/litefs/myapp.db?_journal=WAL")
No app changes. SQLite operations go through the FUSE layer; writes get replicated; reads are local.
The write-path challenge
Reads from any node are great. Writes need to go to the primary. Two patterns to handle this:
1. Proxy-based: LiteFS redirects writes for you
If your app is HTTP, LiteFS has a built-in proxy:
proxy:
addr: ":20202"
target: "localhost:8080"
db: "myapp.db"
passthrough: ["/static/*", "/health"]
Clients hit LiteFS's proxy at :20202. On a replica, the proxy notes if the request mutates the DB; if so, it redirects to the primary. The app code is unchanged; the LiteFS proxy handles routing.
2. App-aware: read the LiteFS env vars
LiteFS exports env vars to the app (FLY_REGION, FLY_PRIMARY_REGION in the Fly.io pattern; or generic LITEFS_PRIMARY=<node-id>). The app checks if it's the primary; if not, returns an HTTP 5xx with a redirect header pointing the client at the primary. Slightly more code, more control.
The replication lag visible to the user
Write to the primary, immediately read from a replica — you may see stale data if the WAL frames haven't replicated yet. Common patterns:
- Read-your-writes via cookie — after a write, set a cookie with the WAL position; replicas can wait for that position before responding. LiteFS exposes the position via headers.
- Force read-from-primary for sensitive operations (post-purchase order-status pages).
- Embrace eventual consistency — for most read-heavy apps, ~100-500ms staleness is fine.
Multi-region deployment shape
The canonical Fly.io shape:
- Primary node in one region (e.g. us-east).
- Replica nodes in N other regions (us-west, eu-west, ap-southeast).
- App container in each region reads from local LiteFS.
- App-aware proxy redirects writes to us-east; reads stay local.
Result: a user in Singapore gets ms-latency reads from the local replica, while writes go to the primary across the Pacific (with the higher latency). For read-heavy apps (90%+ reads, typical), the user-visible experience is excellent.
Lose the primary
The consul-backed lease has a TTL; if the primary dies, the lease expires, and another node grabs it. New primary continues serving writes; replicas re-sync.
Lag during failover: a few seconds while the lease times out + new leader is elected. For most apps, fine; for "absolutely zero downtime," LiteFS isn't the answer (use a real distributed DB like CockroachDB; see that tutorial).
LiteFS Cloud (paid SaaS)
Fly.io's LiteFS Cloud is the hosted version: per-database backup, point-in-time recovery, cross-region replication without operating Consul yourself. Useful if you don't want to run the lease infrastructure.
LiteFS vs Litestream vs rqlite
- Litestream (see that tutorial) — one writer + S3 replicas (DR-only, no live reads from replicas). Simplest setup; no HA reads.
- LiteFS — one writer + N live read replicas across regions. Read scalability + multi-region; complex setup.
- rqlite (see that tutorial) — multi-writer via Raft consensus; strong consistency; but app code talks to rqlite via HTTP, not direct SQLite.
When LiteFS shines
- Read-heavy SQLite apps that need to scale reads geographically.
- Migrations where the app already uses SQLite + you want HA reads without rewriting to Postgres.
- Edge / multi-region deployments on Fly.io or similar platforms.
When it isn't right
- Write-heavy workloads — only one primary writes; throughput is bounded.
- Apps needing strong consistency on reads — replicas are eventually consistent.
- "I want a real distributed SQL" — CockroachDB / TiDB / YugabyteDB are the right shape.
- "I just want SQLite with backups" — Litestream is simpler.