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.