⏱️ Reading time: 14 min
In September 2014, Philip O’Toole pushed a weekend project to GitHub: combining SQLite with a Raft log so writes would replicate across a cluster. Twelve years later, that experiment gave its name to an entire category of software, distributed SQLite, with two acquisitions and venture funding behind it.
📑 En este artículo
- TL;DR
- What is distributed SQLite?
- Why Raft-based SQLite replication matters
- How it works: Raft, WAL streaming, and CRDTs
- The full map: from rqlite to managed platforms
- Comparison: rqlite, dqlite, LiteFS, Litestream, and Turso/libSQL
- Getting started with rqlite
- Real-world use cases
- Common mistakes and best practices
- Going deeper: why the CAP theorem matters here
- Frequently Asked Questions
- References
O’Toole published a full retrospective of the story this year, from rqlite to today’s managed platforms (see References). This article takes that retrospective as a starting point to explain the category and compare the current options: rqlite, dqlite, Litestream, LiteFS, and Turso/libSQL, and when each one makes sense.
TL;DR
- Distributed SQLite combines SQLite with replication (Raft, WAL streaming, or CRDTs) to survive a node going down.
- rqlite, released in 2014, was the first open project to add Raft to SQLite to form a CP cluster.
- dqlite offers the same pattern as rqlite but as a C library, embedded in Canonical’s LXD and MicroK8s.
- Litestream skips consensus entirely: it just streams the WAL to S3 to recover a single node after a crash.
- LiteFS and Turso/libSQL show the other two paths: filesystem-level replicas and an engine fork built for the edge.
What is distributed SQLite?
Distributed SQLite is the category of systems that take the SQLite database engine, designed for a single file in a single process, and add a replication layer across multiple nodes so that one machine going down doesn’t mean losing data or service availability.
The idea itself isn’t new, but the packaging is: before 2014, replicating a relational database meant Postgres, MySQL, or a full distributed system like Cassandra. Distributed SQLite offers the same thing at a much smaller scale, with a single binary and no dedicated operations team.
Why Raft-based SQLite replication matters
SQLite stores the entire database in a single file, and only one process writes at a time. It’s fast, simple, and perfect for a desktop app, a site with moderate traffic, or an embedded device. But if the machine running it goes down, the file goes down with it unless there’s a recent backup.
That specific problem is what O’Toole ran into in 2014 at jut.io: a clustered system needed small relational storage, with no clear way to decide which node should hold it. Replicating SQLite with a consensus protocol solved the question without standing up a separate full database. The need was specific, not a general architecture redesign.
Twelve years later, the same argument still holds for small microservices, edge agents, and applications that don’t justify running Postgres in high availability, but still need to survive the loss of a node without manual intervention.
How it works: Raft, WAL streaming, and CRDTs
O’Toole’s retrospective identifies three distinct approaches to the same problem, and all three appeared in chronological order.
flowchart TD
A["SQLite + replication"] --> B["Consensus: rqlite, dqlite"]
A --> C["WAL streaming: Litestream, LiteFS"]
A --> D["Leaderless merge: cr-sqlite"]
B --> E["CP: consistent, pauses on partition"]
C --> F["Single writer, cheap recovery"]
D --> G["AP: always writable, merges later"]
The first approach: consensus with Raft
rqlite and dqlite use Raft, a consensus protocol where a leader node receives writes, replicates them to the other nodes in the cluster, and only confirms the operation once a majority acknowledges it. If the leader goes down, the cluster elects a new one among the nodes with the most up-to-date log.
This makes both of them CP systems under the CAP theorem: they prioritize consistency over availability, so a network partition where the cluster loses its majority stops accepting writes until the majority is restored.
sequenceDiagram
participant C as Client
participant L as "Leader node"
participant F1 as "Follower node 1"
participant F2 as "Follower node 2"
C->>L: POST /db/execute
L->>F1: replicate log entry
L->>F2: replicate log entry
F1-->>L: ack
F2-->>L: ack
Note over L: majority confirmed, commit
L-->>C: 200 OK
The second approach: WAL streaming
Litestream doesn’t use consensus. It watches SQLite’s write-ahead log (WAL), the file where the engine records every change before applying it to the database, and streams it continuously to storage like S3. If the single node crashes, the database gets rebuilt from that saved WAL: there’s no cluster or quorum, just one writer and a near-real-time copy.
LiteFS, built by the same author as Litestream (Ben Johnson) after he was hired by Fly.io, replicates at the filesystem level and uses Consul leases to decide which node is primary at any given moment.
flowchart LR
A["Local SQLite"] --> B["Writes to the WAL"]
B --> C["Litestream watches the WAL"]
C --> D[("S3 or other object storage")]
E["Node crashes"] --> F["Restores from saved WAL"]
D --> F
F --> A
The third approach: leaderless merge
cr-sqlite adds CRDTs to SQLite as an extension: multiple nodes can write at the same time without coordinating and merge their changes afterward without irreconcilable conflicts. It’s the AP corner of the design, always available for writes, at the cost of consistency that gets resolved later rather than in the moment.
The full map: from rqlite to managed platforms
Before rqlite, Expensify was already replicating SQLite internally with a Paxos-style protocol and released it as Bedrock in 2016. ActorDB, an Erlang-based system, replicated SQLite per actor around the same time as rqlite. Neither saw mass adoption outside its origin, but both confirm the idea was already in the air in 2014.
Canonical built dqlite in 2017 to give LXD, and later MicroK8s, an embedded replicated store without depending on etcd. dqlite started in Go and was rewritten in C with its own Raft library. The difference with rqlite is mostly about packaging: dqlite is a library embedded inside another program, rqlite is a server that runs alongside any application.
Ben Johnson released Litestream in early 2021. His argument, documented in O’Toole’s retrospective, was that most applications don’t need a cluster: they run on a single machine and just need a way to recover if that machine dies. Fly.io hired him in 2022 and launched LiteFS, the first version of replicated SQLite offered as a platform feature rather than something a developer deploys on their own.
libSQL took a different path: instead of adding an external layer, it forked SQLite’s own engine to modify its core, something the SQLite project, closed to outside contributions, doesn’t allow. ChiselStrike built an edge-replicated service on top of that fork and became Turso, which raised venture capital on that pitch. In 2026, Supabase acquired Turso. SQLite Cloud, in parallel, offers hosted SQLite with Raft replication, the option closest to rqlite’s original design but wrapped as a managed service.
Cloudflare took yet another path: it announced D1 in 2022 as serverless SQLite inside its edge network and later added SQLite storage to Durable Objects. At that point the SQLite cluster stopped being something someone runs and became a product someone buys.
Comparison: rqlite, dqlite, LiteFS, Litestream, and Turso/libSQL
The choice depends on the type of availability the application needs: tolerating a node failure with near-instant recovery, or just needing a cheap backup. The table below summarizes when each option makes sense.
| Option | When to use it | Advantage | Limitation |
|---|---|---|---|
| rqlite | Small cluster that needs to keep writing if a node goes down, over HTTP | Standalone server, simple HTTP API, no external dependencies | Writes only through the leader, network latency on every confirmation |
| dqlite | Embedding a replicated store inside another program (like LXD) | C library, integrates without spinning up a separate process | Built to integrate into a product, not to run standalone |
| Litestream | A single server that only needs disaster recovery | Zero coordination, runs nearly free alongside the app | Doesn’t provide high availability: restoring from S3 takes time |
| LiteFS | Apps on Fly.io with a primary writer and read replicas | File-level replication, transparent to SQLite | Tied to the Fly.io ecosystem and to Consul for leases |
| Turso / libSQL | Low-latency reads from the edge, with centralized writes | Managed global replicas, fork with its own extra features | Depends on a vendor and a fork that diverges from official SQLite |
Getting started with rqlite
Of this list, rqlite is the simplest to try on a laptop because it’s a single binary with no dependencies. This guide uses Linux; on macOS, brew install rqlite is enough, and on Windows the simplest option is running it inside WSL with the same steps.
Download the binary from the GitHub releases page (adjust version and architecture) and extract it:
curl -LO https://github.com/rqlite/rqlite/releases/latest/download/rqlite-linux-amd64.tar.gz
tar xzf rqlite-linux-amd64.tar.gz
cd rqlite-*/
Start a first node. The -node-id flag identifies the node within the cluster, and the last argument is the directory where it stores its data:
./rqlited -node-id 1 ~/node1
With the node running, open another terminal and create a table and a row using the HTTP API on port 4001:
curl -XPOST localhost:4001/db/execute -H "Content-Type: application/json" -d '[
"CREATE TABLE registros (id INTEGER PRIMARY KEY, nodo TEXT)",
"INSERT INTO registros (nodo) VALUES (\"node1\")"
]'
The expected response is a JSON object with the result of each statement and how long it took to confirm:
{"results":[{},{"last_insert_id":1,"rows_affected":1}],"time":0.00234}
To read the data, use the query endpoint:
curl -G 'localhost:4001/db/query' --data-urlencode 'q=SELECT * FROM registros'
Which returns something like:
{"results":[{"columns":["id","nodo"],"types":["integer","text"],"values":[[1,"node1"]]}],"time":0.0004}
To confirm that the node is part of a Raft cluster (even a single-node one) and who the leader is, query the status endpoint:
curl localhost:4001/status | python3 -m json.tool | grep -A2 '"raft"'
It should show "state": "Leader" on a single node. If you add a second node with -join http://localhost:4001 and query its status again, that field switches to "state": "Follower", confirming that replication is active.
Real-world use cases
- Small edge cluster: rqlite or dqlite for a service that needs to keep serving reads and writes if one of three machines goes down, without running Postgres in high availability.
- Single-server apps with cheap backup: Litestream running alongside a Rails or Django app that lives on a single VM, so data isn’t lost if the VM gets destroyed.
- Multi-region with a primary writer: LiteFS for apps on Fly.io that read from the region closest to the user but always write against the primary.
- Product embedded in other software: dqlite inside LXD or MicroK8s, where the end developer never interacts directly with the replicated store.
- Low-latency global reads: Turso or SQLite Cloud when the team prefers to pay for a managed service instead of operating the cluster.
Common mistakes and best practices
The most common mistake is treating rqlite or dqlite as a transparent replacement for the local SQLite file. It isn’t: writes go through an HTTP API (rqlite) or the embedded library (dqlite), not through the usual sqlite3 driver. Migrating an existing app means changing the data access layer, not just the configuration file.
Another mistake is ignoring read consistency levels. rqlite lets you read with level none (from any node, possibly stale), weak (confirms the node is still leader), or strong (goes through the Raft log, slower but guarantees the last confirmed write). Using none on a financial report just because it’s the default is the kind of bug that doesn’t show up until a follower falls behind.
⚠️ Heads up: an rqlite or dqlite cluster with an even number of nodes doesn’t improve fault tolerance compared to having one fewer: Raft needs a majority, so 4 nodes tolerate the same failure as 3, with more network latency on every write.
With Litestream, the typical mistake is assuming it replaces a cluster: it only provides single-node disaster recovery, not continuous high availability. If the VM crashes, there’s a real gap (the time it takes to restore from S3 and start the process) before it can serve traffic again.
With libSQL and Turso, check which fork-specific extensions the app uses before assuming full compatibility with standard SQLite: being a fork, some functions don’t exist in original SQLite, and vice versa.
Going deeper: why the CAP theorem matters here
Raft guarantees that, as long as a majority of nodes are available and communicating, the cluster has exactly one leader and every confirmed write survives even if that leader later goes down. That guarantee has a cost: if the network partitions and neither side has a majority, neither side accepts writes until the partition resolves. rqlite and dqlite choose that cost explicitly, the design decision O’Toole points to as the one that hasn’t changed in twelve years.
The streaming corner (Litestream, LiteFS) doesn’t face that dilemma because there aren’t multiple writers competing to confirm at the same time: there’s one writer and a queue of changes that gets replicated outward. The tradeoff is that recovery isn’t instant, it depends on how much WAL needs to be replayed.
The merge corner (cr-sqlite) solves the problem a different way: instead of preventing concurrent writes, it allows them and resolves conflicts with a deterministic rule based on the chosen CRDT. It’s the right design for local-first apps, where each device writes offline and syncs later, but it would be a poor fit for a case that needs a single, immediate source of truth, like a bank balance.
Your next step: spin up a three-node rqlite cluster on your laptop, kill the leader’s process mid-write, and confirm with curl localhost:4003/status that another node took over the role before the ten-second default election timeout.
Frequently Asked Questions
Does rqlite replace Postgres or MySQL?
Not for large workloads. rqlite is meant for small data volumes that need simple high availability, not the full feature set (advanced indexes, extensions, large sizes) of a complete relational database.
Can dqlite be used outside LXD or MicroK8s?
Yes, it’s a general-purpose C library, but its design and documentation are meant for embedding inside another program, not for running as a standalone service the way rqlite does.
Does Litestream provide high availability?
No. Litestream provides single-node disaster recovery: if that node goes down, you have to restore from external storage before serving traffic again, however long that takes.
Does LiteFS depend on Fly.io to work?
LiteFS is open source and can run outside Fly.io, but its Consul lease integration and documentation assume that environment; outside it, you have to solve that coordination yourself.
Are Turso and libSQL 100% compatible with SQLite?
libSQL is a fork of SQLite, so it adds and modifies functions that the original SQLite project doesn’t accept as outside contributions. Compatibility is high but not total: check which extensions the app uses before migrating.
When should you use cr-sqlite instead of rqlite?
When the app needs multiple devices to write offline and sync later (local-first), not when it needs a single, immediate source of truth the way rqlite provides with Raft.
References
- Twelve years of distributed SQLite, Philip O’Toole: the full retrospective that inspired this article.
- rqlite on GitHub: official repository, HTTP API documentation, and releases to download.
- dqlite on GitHub: Canonical’s official repository with the C library.
- Litestream, official site: installation and S3 configuration documentation.
- libSQL on GitHub: the SQLite fork used by Turso.
- Write-Ahead Logging, official SQLite documentation: how the WAL that Litestream and LiteFS watch actually works.
📱 Enjoying this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.
Featured image: Foto de imgix en Unsplash
Did it work for you? Got a different error? Say so below: questions get answered and help the next reader.
Leave a comment
0 Comments