SQLite in production in 2026: revisit Litestream, rqlite and LiteFS before you add another database server for a small app

Picture a small software company in Madurai that builds a job card app for about 60 two-wheeler service centres across southern Tamil Nadu (an invented example, not a real client). Mechanics open a job card on a tablet, add spare parts as they fit them, and the customer gets a message when the vehicle is ready. The whole backend is one Go service on one virtual machine, and the data lives in a single SQLite file of about 3 GB. On a busy Saturday it sees perhaps 20 writes a second and many more reads. The team is three developers, one of whom also looks after the servers.

Last month the cloud provider had a host failure. The VM came back on another host, but its disk did not, and the newest copy of the database was the provider’s nightly disk snapshot, taken about 22 hours earlier. The team rebuilt the missing job cards from WhatsApp messages and paper slips over a weekend. Now the managing director asks whether it is time to “move to a proper database server”, and one developer wants to try a distributed SQLite instead.

This post compares Litestream, rqlite and LiteFS as of 20 October 2026, with libSQL and Turso where they change the picture, and with the plain SQLite features that already cover part of the job: WAL mode, the online backup API, VACUUM INTO and sqlite3_rsync. It covers versions and maintenance status, licences, the write path, the data loss window, failover, read consistency, restore from object storage, monitoring, advisories and a drill.

The facts come from primary material: SQLite’s documentation and release history, GitHub release data, the projects’ own docs and changelogs, Fly.io and Turso announcements, and NVD and GitHub advisory records. The drill ran Litestream 0.5.17, rqlite 10.6.2, LiteFS 0.5.14 and the SQLite 3.54.1 tools on one shared box, on loopback only. Where no primary source exists, the post says “Not documented”. Remarks on licences are my interpretation and not legal advice.

The short version

  • SQLite is not the risky part; the single machine is. A WAL-mode file is consistent after a crash. What it cannot give you alone is a copy elsewhere, a second machine that can take writes, or a practised restore.
  • Litestream is the simplest answer, and it is a backup, not a cluster. It streams changes to object storage every second by default, so a machine loss costs about the last second of writes, and nothing takes over writes for you. The 0.5 line (from 1 October 2025) allows one replica per database.
  • rqlite is a real cluster, and not a drop-in SQLite. It replicates SQL statements with Raft, waits for a majority before acknowledging a write, and failed over in about two seconds in the drill. You talk to it over HTTP and must pick read consistency on purpose.
  • LiteFS works, but it is no longer moving. Its last release and last main-branch commit are from 23 April 2025, and the Fly.io docs now say “We are not able to provide support or guidance for this product”.
  • libSQL and Turso are changing direction. The libSQL README points new projects to Turso Database, a Rust rewrite not yet at 1.0, and on 2 October 2026 Turso announced that Supabase is acquiring it.
  • Check the SQLite version inside your driver. The “WAL-reset bug” (3.7.0 through 3.51.2, fixed in 3.51.3 on 13 March 2026) needs two connections in different threads or processes writing or checkpointing at the same instant. A replication process next to your app is that second connection.

Where things stand on 20 October 2026

Every time below is in IST. GitHub published_at stamps were shifted from UTC to IST; a few dates come from changelogs, and I say so. Star counts were read on 20 October and show attention, not quality.

SQLite itself

  • Latest: 3.54.1 (16 October 2026), a fix release for 3.54.0 (9 October 2026). The source is in the public domain.
  • WAL-reset bug: found on 3 March 2026, “likely present in all version of SQLite from 3.7.0 (2010-07-21) through 3.51.2”, fixed in 3.51.3 (13 March 2026), with backports in 3.44.6 and 3.50.7. The SQLite page calls it rare and “not an emergency”, but asks developers to upgrade. The drill box’s Python sqlite3 module reported 3.46.1, which is inside that range.
  • Built-in copy tools: the online backup API; VACUUM INTO (since 3.27.0, February 2019); and sqlite3_rsync, experimental from 3.47.0 (October 2024), whose WAL-mode and page-size limits were removed in 3.50.0 (May 2025).

Litestream

  • Latest: v0.5.17 (1 September 2026, 3:29 AM). Its notes include “remove release-blocking SQLite WAL-reset corruption exposure”. The main branch had commits as recent as 19 October 2026, 10:32 PM.
  • The 0.5 line: v0.5.0 arrived on 1 October 2025, 4:14 AM. It replaced the old “generations” and shadow WAL with LTX files and multi-level compaction, dropped several replicas per database, renamed the wal command to ltx, added NATS JetStream replicas and a first VFS, and moved to a pure Go SQLite driver. Age encryption is not available in 0.5.
  • The old line: v0.3.14 (26 March 2026, 8:30 PM) fixed one restore panic for those still on 0.3.
  • Licence and attention: Apache-2.0; 13 releases dated 2026; 14,506 stars. The release notes say Windows binaries are provided “for convenience” and Windows is not an officially supported platform.

rqlite

  • Latest: v10.6.2 (18 October 2026, 10:21 PM). The binary in the drill reported Go 1.27.2 and SQLite 3.53.4.
  • v10: v10.0.0 is dated 27 April 2026 in the changelog. It made Raft snapshotting non-blocking, used CRC32 checks more widely, added a web console and removed -on-disk-path. A v10 node cannot join a v9 cluster.
  • v10.6.0 (12 October 2026, 6:32 PM): numeric error_v2 codes, promote and demote of voting nodes, and a fix where “a user with no explicit permissions could be granted elevated permissions” through a misconfigured auth file.
  • Licence and attention: MIT, copyright Philip O’Toole 2015 to 2026; in development since 2014; 53 releases dated 2026 in the changelog; 17,808 stars.

LiteFS

  • Latest: v0.5.14 (23 April 2025, 12:44 AM), one of three releases that day after a 15-month gap. No releases in 2026; the latest commit on the main branch is from the same night.
  • Status in its own words: the README says “actively maintained but is currently in a beta state”. The Fly.io documentation says “We are not able to provide support or guidance for this product. Use with caution”, and warns against combining LiteFS with Fly Machines autostop and autostart because a stale machine could win the lease and discard newer changes.
  • LiteFS Cloud: the managed backup service was wound down; Fly.io said it would stay available until 15 October 2024 and suggested Litestream or Turso for disaster recovery.
  • Licence and attention: Apache-2.0; 4,897 stars.

libSQL and Turso

  • libSQL: an MIT-licensed fork of SQLite with embedded replicas and a server for remote access. Its README now says libSQL “is actively maintained, but new features are being developed in Turso”, and that new projects “probably want to look into Turso”. The last GitHub release of libsql-server is v0.24.32 (14 February 2025, 6:29 PM).
  • Turso Database: an MIT-licensed rewrite in Rust that aims at compatibility with SQLite’s SQL, file format and C API, with BEGIN CONCURRENT writes. Its FAQ says it has “not yet reached 1.0” and asks users to keep independent backups. Latest stable release v0.8.3 (12 October 2026, 8:33 PM); a v0.9.0 pre-release followed on 15 October.
  • Ownership: Turso’s blog of 2 October 2026 says Supabase is acquiring Turso and that “Turso Database remains open source and actively developed”.

First question: what do you lose, who writes next, how fast do you come back?

When a team says “SQLite is not production ready”, they usually mean three separate worries.

  1. Durability. If the machine dies at the worst moment, how many committed writes are gone? That is your recovery point objective (RPO). It depends on how SQLite syncs to the local disk and how often the changes leave the machine.
  2. The write path. SQLite allows one writer at a time per file. With more than one machine, something must decide which is primary, stop the others from writing, and move that role when the primary dies, without two machines believing they are primary at once.
  3. Restore. How long does it take to get a working database on a new machine, and has anyone done it since the last schema change? That is your recovery time objective (RTO), known only if measured.

Litestream answers durability and restore, and leaves the write path to you. rqlite answers all three, at the price of an HTTP API and a cluster to run. LiteFS answers replication within a cluster and the write path, with no backup of its own since LiteFS Cloud ended.

So the honest promise for a small app on SQLite is not “we use SQLite in WAL mode”:

“If this machine is lost at the worst possible moment, we know how many seconds of writes are gone, we know which machine may write next and how it is told, and we know how long a restore takes, because we measured all three this month.”

If any part of that sentence is a guess, that is the part to work on before anyone buys a bigger database server.

What each one actually is

Plain SQLite features

Before any extra tool, SQLite can already make safe copies of a live database. The online backup API (the .backup command in the CLI) copies pages into another file, and restarts if another connection writes to the source during the copy. VACUUM INTO 'file' writes a compacted, consistent snapshot; the docs note it uses more CPU than the backup API but produces a minimal file. **sqlite3_rsync ORIGIN REPLICA** brings a replica up to date with the origin, over SSH if needed, with a bandwidth-efficient protocol while both stay live. What none of them does is continuous replication or failover. Copying the raw database file with cp while a transaction is open is listed on the “How To Corrupt An SQLite Database File” page.

Litestream 0.5.17

Litestream is a separate process that runs beside your application. It holds a long-running read transaction so that no other connection can checkpoint the WAL, packages new WAL pages into LTX files with an increasing transaction ID (TXID) and checksums, performs the checkpoints itself, and uploads the files to one replica: S3 or an S3-compatible store, Google Cloud Storage, Azure Blob Storage, Alibaba OSS, SFTP, WebDAV, NATS JetStream or a local path. A background compactor merges files into levels (30 seconds, 5 minutes and 1 hour by default) and takes a full snapshot every 24 hours by default. Restore fetches the newest snapshot that does not overshoot the target and replays LTX files in order, checking that no TXID is missing.

Your application does not change, apart from pragmas the docs recommend: busy_timeout of about 5 seconds, because Litestream briefly takes the write lock while checkpointing, and, under heavy write loads, wal_autocheckpoint = 0.

rqlite 10.6.2

rqlite is a server, rqlited, that embeds SQLite and puts Raft in front of it. A client sends SQL over HTTP to any node; writes are forwarded to the leader, written to the Raft log with an fsync, replicated to a majority, and then applied to each node’s SQLite copy. Because it replicates statements, rqlite rewrites non-deterministic parts such as RANDOM(), RANDOMBLOB(N) and now before they enter the log. Inside each node SQLite runs with synchronous=OFF for speed; rqlite periodically syncs a known-good copy and can rebuild the database from the Raft log if needed. Three, five, seven or nine voting nodes are the practical sizes, and non-voting read replicas can be added on top.

LiteFS 0.5.14

LiteFS is a FUSE file system. Your application opens a database under the LiteFS mount as if it were a normal file; LiteFS sees each transaction’s pages, writes them as LTX files, and streams them to the other nodes. One node holds a lease and is the primary; writes on any other node fail. The lease comes from Consul, so the primary can move, or is “static”, so one fixed node is always the primary. Replication is asynchronous. If an old primary comes back with writes nobody else saw, LiteFS detects the divergence through a rolling checksum and takes a fresh snapshot from the new primary. An optional built-in HTTP proxy forwards write requests to the primary and tracks positions so that a client’s reads on a replica are consistent with its own writes.

libSQL and Turso Database

libSQL keeps SQLite’s engine and adds embedded replicas, which sync a local copy from a remote primary, and sqld for remote access. Turso Database is a Rust implementation that reads and writes SQLite files and adds concurrent writes through MVCC, plus a cloud service with sync. Neither is part of the drill.

Side by side

If the projects’ own pages are silent on a point, the entry reads “Not documented”. Results labelled as drill results come from the one shared box described further down, on loopback, and are single runs.

Licence and governance

  • Litestream: Apache-2.0, in the personal GitHub account of its author, Ben Johnson. Its May 2025 revamp was announced on the Fly.io blog, but it is not tied to Fly.io.
  • rqlite: MIT, copyright Philip O’Toole, its creator and lead maintainer, with outside contributors.
  • LiteFS: Apache-2.0, in the superfly organisation, with no releases since April 2025 and no support offered by Fly.io.
  • libSQL and Turso Database: MIT, from Turso, which Supabase is acquiring. SQLite itself is public domain.
  • In practice: all are free to run. The real question is who will fix a bug in your restore path in 2027. My reading, not legal advice.

Architecture and write path

  • Litestream: one machine, one writer, exactly as plain SQLite. Litestream adds no second writer and no failover. Two machines replicating into the same bucket path can leave a replica you cannot restore, and the docs make preventing that your job.
  • rqlite: every write goes through the leader and a Raft majority. Any node accepts a write and forwards it; if no leader is reachable, the write fails. Writes are blocked while a Raft snapshot is taken and during VACUUM.
  • LiteFS: one primary at a time, chosen by the lease. Writes on a replica fail with an I/O error in the application, so the app or the LiteFS proxy must send writes to the primary.
  • Plain SQLite: one writer per file; BEGIN IMMEDIATE and busy_timeout are the tools.

Durability and RPO

  • Local disk first: in WAL mode, synchronous=NORMAL stays consistent, but the SQLite docs say “a transaction committed in WAL mode with synchronous=NORMAL might roll back following a power loss or system crash”. Use FULL if the last transaction must survive a power cut on that machine. Both survive an application crash.
  • Litestream: asynchronous. The default sync-interval is 1 second, and the docs say a catastrophic crash loses the changes in that window; synchronous replication is “on the Litestream roadmap but has not yet been implemented”. On a normal shutdown it tries to sync everything first. Drill: SIGKILL lost the last 0.975 seconds (47 rows); SIGTERM lost nothing. If the replica target is failing, the window grows until someone notices.
  • rqlite: an acknowledged write is in the Raft log on a majority, and Raft calls fsync after each log write. Queued writes give that up: the API returns before the Raft commit, and the docs describe “a small risk of data loss in the event the node crashes”. Drill: 785 acknowledged writes, none missing after the leader was killed.
  • LiteFS: asynchronous within the cluster; unreplicated writes on a failed primary are lost when another node takes over. Synchronous replication is described as planned. Drill: 351 acknowledged writes, none missing after the primary was killed, with replicas about 2 milliseconds behind on loopback. That is the result of one run, not a guarantee.

Failover

  • Litestream: none. Recovery is a restore onto another machine, by hand or with restore-if-db-not-exists at start-up (since 0.5.6). The 2025 Fly.io post described a lease based on conditional writes to object storage; I did not find it in the configuration reference, so as a user-facing feature: Not documented.
  • rqlite: automatic through Raft. Drill: about 1.9 seconds from killing the leader to the next successful write, with 89 failed requests in between. When a majority is lost, writes and normal reads stop.
  • LiteFS with Consul: automatic after the lease expires. Consul does not allow a TTL below 10 seconds. Drill: 21.1 seconds from killing the primary to the first successful write on the other node, with ttl: 10s and lock-delay: 1s.
  • LiteFS with a static lease: none. The replica keeps serving reads, and writes wait for the primary to come back.

Reads and consistency

  • Litestream: reads come from the one local file. Read replicas are possible with restore -f (follow mode) or the experimental read-only VFS that fetches pages from the replica storage, both a sync interval or more behind.
  • rqlite: you choose per request. weak (default) checks locally that the node is leader and forwards if not; linearizable confirms leadership with a quorum heartbeat; strong sends the read through the Raft log and the docs say “Don’t use Strong in production code”; none reads the local copy with no checks, optionally limited by freshness; auto picks none on read replicas and weak elsewhere. Drill: a none read on a follower straight after an acknowledged write missed the whole batch five times out of five; followers caught up in about 57 to 99 milliseconds.
  • LiteFS: every node reads its local copy, which may lag the primary. Each database has a -pos file with TXID and checksum, so an application can compare positions, and the proxy uses them to keep a client’s reads consistent with its writes.

Restore

  • Litestream: litestream restore to a TXID or timestamp, with -dry-run (shows the plan), -integrity-check quick|full (since 0.5.10) and parallel downloads (8 by default). Restore points are LTX file boundaries, and they get coarser as files are compacted and old ones are pruned (L0 files are kept for 5 minutes by default). Drill: a 205 MB database came back from the S3 target in about 1.5 seconds; a timestamp restore landed about one second before the requested time.
  • rqlite: hot backups through /db/backup (SQLite file, SQL text, vacuumed or compressed), automatic uploads to S3, GCS or a directory (only the leader uploads, and only if the data changed), and -auto-restore for a new node. A failed streaming backup is signalled with an X-Stream-Error trailer, not an HTTP status. Drill: auto-restore of a 155 MB compressed backup took about 10.5 seconds to a ready node.
  • LiteFS: litefs export and litefs import move a database out of and into a cluster. Off-site backup is now your job; Fly.io suggested running Litestream against the primary.
  • Plain SQLite: whatever copies you make with the backup API, VACUUM INTO or sqlite3_rsync, as often as you run them. The RPO is the gap between copies.

Operations and monitoring

  • Litestream: a Prometheus endpoint when addr is set, litestream status, litestream ltx and logs. Drill warning: when the S3 target ran out of space, status kept saying ok and litestream_sync_error_count stayed at 0 while nothing reached the replica; the first error in the log came about two minutes later. Watch the replica side as well: the age of the newest LTX file in the bucket.
  • rqlite: /status, /nodes, /readyz, a web console since 10.0, and OTLP metrics push to an OpenTelemetry Collector since 10.3.0. Disk is the main thing to watch: in the drill a 205 MB database used 392 MB in the node’s data directory.
  • LiteFS: per-database position files and a .lag file in the mount, the HTTP API, and logs. A FUSE mount needs /dev/fuse, and a dead mount needs cleaning up before the node can start again (drill).

Release rhythm and upgrade risk

  • Litestream: frequent 0.5.x releases (13 in 2026), many of them Go toolchain bumps for govulncheck findings. Moving from 0.3 to 0.5 changes the replica format and drops several replicas per database and Age encryption, so plan it as a migration with a full new snapshot.
  • rqlite: very frequent patch releases (53 in 2026). Major versions change command-line flags; v10 nodes cannot join v9 clusters, and downgrading means restoring a backup into a fresh older cluster.
  • LiteFS: no releases for 18 months. That is low upgrade risk and high risk that nobody fixes the next bug.
  • SQLite: a minor release every few months. Your SQLite version is usually decided by your language driver, so check it there.

Worked example for the Madurai scenario

The Madurai team’s trouble was never SQLite. It had no copy that left the machine more often than once a night, and nobody had ever restored one.

Step 1: keep one machine, configured on purpose. At 20 writes a second on a 3 GB file, one VM with local SSD is far from SQLite’s limits. Use WAL mode, busy_timeout, foreign_keys = ON on every connection, and BEGIN IMMEDIATE for writing transactions. Choose synchronous = FULL: it costs little at this rate and keeps the last job card through a power cut.

Step 2: check the SQLite inside the driver. The Go service’s driver bundles its own SQLite. Confirm with SELECT sqlite_version() that it is 3.51.3 or later, or one of the backport releases, before adding a second process that checkpoints the same file.

Step 3: add Litestream 0.5.17 with one off-site replica. One S3-compatible bucket in a different region from the VM, a dedicated access key that can write only that prefix, retention.enabled: false with a lifecycle rule in the bucket so the key needs no delete permission, metrics on a local port, and the pragmas above:

# /etc/litestream.yml (Madurai, invented)
addr: "127.0.0.1:9090"
retention:
  enabled: false          # bucket lifecycle rule removes old files instead
snapshot:
  interval: 6h
dbs:
  - path: /srv/jobcard/data/jobcard.db
    busy-timeout: 5s
    replica:
      url: s3://jobcard-backup/jobcard
      sync-interval: 1s

# every app connection
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
PRAGMA synchronous = FULL;

# monthly drill on a spare VM
litestream restore -dry-run -o /tmp/r.db s3://jobcard-backup/jobcard
time litestream restore -integrity-check full -o /tmp/r.db s3://jobcard-backup/jobcard
sqlite3 /tmp/r.db "SELECT count(*), max(updated_at) FROM job_cards;"

Step 4: keep a second, independent copy. Litestream 0.5 allows one replica per database, so a nightly VACUUM INTO to a dated file, pushed to a different provider by the team’s existing backup tool, gives a copy that depends on neither Litestream’s format nor one bucket.

Step 5: write the restore runbook and time it. Who declares the old VM lost, who restores on the new VM, how DNS moves, and how the app is kept from writing on two machines. Run it on a spare VM each month and record the minutes: an RPO of about a second and an RTO under 30 minutes, measured.

Step 6: alert on the copy, not the process. A check reads the newest object time under jobcard/ and pages someone if it is over five minutes old in business hours. That catches an expired key, a full bucket and a stopped process alike.

Step 7: know what would make rqlite worth it. If 30 minutes of downtime starts to hurt more than a second of lost writes, the next step is a three-node rqlite cluster across three zones, with the app moved to its HTTP API, weak reads by default and linearizable where a mechanic must see their own last write. That is a project, not a configuration change. LiteFS is not on the list for new work.

Security and hygiene

Replication tools hold copies of all your data and the credentials to where those copies live. Here is what the advisory records show.

Litestream. CVE-2024-41254 (NVD published 1 August 2024, 2:45 AM; NVD score 5.3, medium): Litestream v0.3.13 used ssh.InsecureIgnoreHostKey() for SFTP replicas, so the SSH host key was not checked and a man-in-the-middle could see replica data. Since v0.5.3 the SFTP replica has a host-key setting; the docs say that “when empty, the host key is not verified”, so set it. The repository has no published security advisories, and v0.5.16 documented private vulnerability reporting.

rqlite. No CVE or GitHub advisory found in NVD, OSV or the repository on 20 October 2026. The v10.6.0 notes describe a security fix (PR #2956): with a misconfigured auth file, a user with no explicit permissions could be granted elevated ones. If you use an auth file, run 10.6.0 or later.

LiteFS. No CVE or advisory found. The bigger risk is no maintenance: no release in 18 months, so dependency fixes are not arriving.

libSQL and Turso. GHSA-8m95-fffc-h4c5, CVE-2025-47736 (9 May 2025, 12:02 PM, low): the libsql-sqlite3-parser crate through 0.13.0 could crash on input that is not valid UTF-8. No advisory found for Turso Database.

SQLite. Not an advisory, but the WAL-reset bug is a corruption risk from 3.7.0 to 3.51.2 that matters most when a second process checkpoints. Litestream 0.5.17 removed its own exposure; your driver carries a separate copy of SQLite.

Habits that matter more than the version number

  • Give the replication process its own object storage key, limited to one bucket prefix, and remove delete permission where the tool allows it (Litestream’s retention.enabled: false).
  • Encrypt replicas at the storage layer if the data is personal; Litestream 0.5 has no Age encryption.
  • Never let two machines replicate into the same Litestream path; make the restore runbook stop the old writer first.
  • For rqlite, expose only the HTTP port to clients, keep the Raft port between nodes, turn on TLS and an auth file, and send reads to /db/query, which runs on a read-only connection.
  • Pin the SFTP host key and keep secrets out of the YAML file with environment variables.
  • Treat every restored file as untrusted until PRAGMA integrity_check returns ok.

Decision guide

Lean towards plain SQLite tools if

  • Losing a few hours of writes is acceptable, and a scheduled VACUUM INTO or sqlite3_rsync copy with a tested restore is enough.

Lean towards Litestream if

  • One machine is fine for writes, and what you want is a copy off the machine every second and a fast restore from object storage.
  • You can accept that a machine loss costs about one sync interval of writes, and that recovery is a restore, not a failover.
  • You will alert on the age of the newest replica file and practise the restore.

Lean towards rqlite if

  • Writes must continue within seconds when a machine dies, and you can run three machines in different failure zones.
  • Your application can talk HTTP to the database, and you are ready to think about read consistency levels and non-deterministic SQL.
  • Your write rate fits a Raft log with an fsync per entry, or you can batch writes.

Lean towards LiteFS only if

  • You already run it, it works, and you have a plan for backups and for moving off it. For new work in October 2026, the maintenance status argues against it.

Look at libSQL or Turso if

  • You want a hosted SQLite service, embedded replicas at the edge, or concurrent writes, and you are comfortable with a vendor going through an acquisition and an engine that is not yet 1.0.

Madurai (invented) pick. Stay on one VM with SQLite in WAL mode and synchronous = FULL; add Litestream 0.5.17 to an S3-compatible bucket in another region with a key that cannot delete; keep a nightly VACUUM INTO copy with a different provider; alert on the age of the newest replica object; and restore on a spare VM every month. Revisit rqlite only if downtime starts to cost more than a second of writes.

Checklist

  1. Every SQLite database in production is listed with its size, write rate, journal_mode, synchronous setting and owner.
  2. Every driver and tool touching those files runs SQLite 3.51.3 or later, or a fixed backport.
  3. At least one copy leaves the machine continuously or on a schedule that matches the RPO you have agreed with the business.
  4. Only one writer per database is possible, and the failover or restore runbook says how the old writer is stopped.
  5. The replica’s credentials can write only their own prefix and, where possible, cannot delete.
  6. An alert fires when the newest replica file is older than a few minutes, independent of the replication process itself.
  7. A restore to a fresh machine has been timed this month and passed PRAGMA integrity_check.
  8. For rqlite, every read path has a chosen consistency level, and no schema relies on per-node defaults like DEFAULT (julianday('now')).
  9. For rqlite, writes check the error field in the JSON body, not only the HTTP status.
  10. Someone reads the SQLite, Litestream, rqlite and driver release notes each month.

Common mistakes

  1. Treating the cloud provider’s nightly disk snapshot as the backup and discovering a 22-hour RPO on the worst day.
  2. Copying the live database file with cp or a file-level backup tool while transactions are running.
  3. Running synchronous = NORMAL in WAL mode because a blog said so, without knowing that the last transactions can roll back after a power cut.
  4. Starting Litestream on a new machine against the same replica path while the old one may still be running.
  5. Trusting litestream status saying ok as proof that the replica is current.
  6. Leaving the SFTP host-key empty, or giving the S3 key permission to delete everything.
  7. Reading from an rqlite follower with level=none and expecting to see a write the same request just made.
  8. Using level=strong in production rqlite code; the docs say to use linearizable instead.
  9. Relying on column defaults with now in rqlite and expecting identical values on every node.
  10. Starting new work on LiteFS in late 2026 without a plan for the day a bug needs fixing.

Drill on this box, 20 October 2026

Setting: a shared Debian 13 virtual machine with 8 vCPUs and 15 GiB of memory, between about 6:26 PM and 6:44 PM IST, on 127.0.0.1 and unprivileged ports only. Each timing is one run with a warm page cache, so treat the numbers as orders of magnitude, not benchmarks. Process kills only partly simulate a machine loss: the page cache survived, so synchronous was not exercised, and I simulated the lost disk by deleting the local files.

Install. GitHub release binaries for Litestream, rqlite and LiteFS; the SQLite 3.54.1 tools from sqlite.org; Consul 2.0.4 for the LiteFS lease. The MinIO server download answered HTTP 410, so the S3 target was SeaweedFS 4.48 on 127.0.0.1:18333. The test writer was Python’s sqlite3 module (SQLite 3.46.1), one row per transaction, synchronous=FULL. The core commands:

weed server -dir=weed-data -ip=127.0.0.1 -s3 -s3.port=18333 ...
litestream replicate -config ls/litestream.yml       # s3, endpoint 127.0.0.1:18333, sync-interval 1s
python3 writer.py ls/app.db 50 120                   # then kill -9 writer and litestream together
litestream restore -integrity-check full -o ls/restored1.db ls/app.db
litestream restore -timestamp 2026-10-20T13:04:31Z -o ls/pitr.db ls/app.db
rqlited -node-id n1 -http-addr 127.0.0.1:14101 -raft-addr 127.0.0.1:14201 rq/data1
rqlited -node-id n2 -http-addr 127.0.0.1:14102 -raft-addr 127.0.0.1:14202 -join 127.0.0.1:14201 rq/data2
consul agent -dev -bind=127.0.0.1 -client=127.0.0.1 -hcl 'ports { http = 18500 ... }'
litefs mount -config fs/c.yml                         # lease type consul, ttl 10s, lock-delay 1s

Litestream, hard kill. The writer managed about 48 rows a second. At 6:29:18 PM I killed the writer and Litestream together with SIGKILL. The local file had 1,919 rows. After deleting the local files, a restore from the S3 target had 1,872 rows and passed the full integrity check: 47 rows lost, the last 0.975 seconds of writes, in line with the 1-second sync interval. The restore and an ltx listing together took about 0.15 seconds.

Litestream, clean stop. With SIGTERM, Litestream exited 0.05 seconds later, and the restore had all 942 committed rows.

Litestream, when the target fails. My first attempt with a 205 MB database (200,000 rows) used a SeaweedFS instance I had sized too small. For about two minutes Litestream logged nothing beyond start-up at the default info level. At 6:32:14 PM came the first error: “compaction failed” for the level 9 snapshot, an S3 UploadPart that “exceeded maximum number of attempts, 10” with HTTP 500. During all of this litestream status showed ok, litestream_sync_error_count stayed at 0, and litestream ltx listed no files on the replica.

Litestream, a bigger restore. After resizing the target, the first snapshot of the 205 MB database was complete about 4 seconds after start. Three restores took 1.60, 1.56 and 1.47 seconds; with -integrity-check full, 1.72 seconds.

Litestream, point in time. With the writer at 20 rows a second, I restored to 6:34:31 PM IST. The result ended at a row written at about 6:34:30.04 PM, while the live database had 18 more rows written before 6:34:31. Restore points are file boundaries, here about one second apart.

Litestream, two replicas. A config with two replicas failed with “multiple replicas on a single database are no longer supported”.

Plain SQLite on the 205 MB file, with Litestream attached. VACUUM INTO took 0.32 seconds, .backup 0.28 seconds, and sqlite3_rsync to a new local file 0.56 seconds. A later sqlite3_rsync -v run reported “sent 4,114 bytes, received 1,111 bytes” for a total size of 205,332,480 bytes. Every copy passed quick_check.

rqlite, writes. Three nodes on default settings. 300 single-row writes had a median of 0.77 ms on the leader and 0.91 ms through a follower. One 300-row bulk request took 4.02 ms; 300 queued writes were acknowledged in 0.072 seconds. Loopback has no network delay, so real clusters will be slower.

rqlite, reads. 200 reads per level on a follower, median: none 0.28 ms, weak 0.32 ms, linearizable 0.36 ms, strong 0.76 ms, auto 0.25 ms. Then, five times, I wrote 500 rows through one follower and immediately read the count from the other with level=none: it missed the whole batch every time, while weak saw it. Over ten single-row trials the follower showed the row 57 to 99 milliseconds after the acknowledgement (median 73.7).

rqlite, non-deterministic SQL. A row that relied on DEFAULT (julianday('now')) stored a different time on each node, up to about 86 milliseconds apart. Values set with julianday('now') or RANDOM() in the statement itself were identical everywhere.

rqlite, failover. With writes every 20 milliseconds alternating between the two followers, I killed the leader with SIGKILL. A new leader took over and the first write succeeded 1.91 seconds after the kill, with 89 failed requests in between. All 785 acknowledged writes were present on both survivors. When I then killed a second node, writes and weak or linearizable reads on the last node returned HTTP 503 “leader not found”, level=none returned 1,986 rows, and level=none&freshness=1s returned HTTP 200 with "error":"stale read" in the body. Restarting one node brought writes back 1.54 seconds later.

rqlite, backup and restore. A single node loaded the 205 MB file through /boot in 0.84 seconds; /db/backup then took 0.12 seconds, and 0.52 seconds compressed (155 MB). The data directory used 392 MB. With -auto-backup to S3 every 10 seconds, the first 155 MB upload took 1.06 seconds. A fresh node with -auto-restore downloaded it in 1.15 seconds, loaded it in 7.18 seconds and was ready with 200,000 rows about 10.5 seconds after start.

LiteFS, static lease. FUSE mounts worked as an ordinary user. 1,000 rows written on the primary appeared on the replica with matching positions (0000000000000003/b3d65ab05450d70b). A write on the replica failed with “disk I/O error” in sqlite3, and LiteFS logged “wal error: read only replica”. Over 50 trials the replica saw a new row 2.2 milliseconds after commit (median; maximum 5.1). A single insert through FUSE had a median of 1.19 milliseconds, against 0.121 milliseconds on the local disk with synchronous=FULL. After SIGKILL of the primary, the replica still served 1,350 rows and refused writes until the primary returned; after the restart both nodes had 1,351.

LiteFS, Consul lease. With a writer at about 50 rows a second on the primary, I killed it with SIGKILL at about 6:42 PM. The other node logged “primary lease acquired” and accepted its first write 21.14 seconds later, after 208 failed attempts. All 351 acknowledged rows were present. Restarted, the killed node rejoined as a replica, and after one more write both nodes had 352 rows.

A box quirk. This box has no /etc/mtab, so fusermount3 could not unmount, even on a clean shutdown. A mount left by a killed LiteFS showed “Transport endpoint is not connected” and blocked a restart until I removed it. That is this environment, not necessarily LiteFS, but test it on your own hosts.

Clean-up. All drill processes stopped and all FUSE mounts were removed by 6:44 PM; large test files were deleted.

What this box could not test: a real power cut or kernel crash (so synchronous settings were not exercised); a real cloud object store such as AWS S3, GCS or Azure, and Litestream’s SFTP, WebDAV and NATS replicas; the Litestream VFS read replicas and any lease based on conditional writes; Windows; rqlite on a real network, with TLS, an auth file, CDC, auto-clustering, read replicas or a v9 to v10 upgrade; the LiteFS proxy, LiteFS on Fly.io and LiteFS with Litestream for backup; and libSQL, sqld, Turso Database or Turso Cloud, none of which I ran.

Unlearn

  • Unlearn that SQLite in production means a corruption risk; in WAL mode the file survives crashes, and the risk is having only one copy.
  • Unlearn that a nightly disk snapshot is a database backup.
  • Unlearn that “replicated” means “nothing is lost”; Litestream and LiteFS are asynchronous.
  • Unlearn that any node in a cluster gives you fresh data on every read.
  • Unlearn that a quiet log and a status of ok mean the copy is leaving the machine.

Re-learn

  • Re-learn RPO and RTO as numbers you measure: seconds of writes lost, minutes to restore.
  • Re-learn synchronous, busy_timeout and the one-writer rule as part of the design, not tuning.
  • Re-learn object storage as the cheap, durable second site for a small database.
  • Re-learn read consistency levels as a choice made per query.
  • Re-learn to check the maintenance status of a tool before you build on it.

Closing

A small app does not need a database server to be safe. It needs a copy that leaves the machine quickly, one writer that everybody agrees on, and a restore that someone has timed. Litestream 0.5.17 gives an off-site copy about a second behind and a fast restore, as long as someone watches the bucket. rqlite 10.6.2 gives failover in a couple of seconds and acknowledged writes that survive, at the cost of an HTTP API, three nodes and careful reads. LiteFS 0.5.14 still works, but with no release since April 2025 and no support from Fly.io, it is a tool to move off rather than start on. libSQL and Turso are worth watching as they settle under Supabase. For the object storage that holds these copies, the post on MinIO and its alternatives compares self-hosted S3 options, and when an app outgrows SQLite, the post on PostgreSQL backups with pgBackRest, WAL-G and Barman applies the same habit of a practised restore. Learn the three questions. Unlearn the nightly snapshot. Re-learn RPO and RTO as measurements. Practise the restore on a spare machine. Apply it before you add another database server for a small app.

Sources

SQLite

Litestream

rqlite

LiteFS

libSQL and Turso

Security records

Drill tools

Internal

comments powered by Disqus

Releted Posts

Log storage in 2026: revisit Grafana Loki, VictoriaLogs and OpenObserve before you keep another month of logs

Picture a platform team in Pune that runs a B2B lending stack for NBFCs across Maharashtra and Gujarat (an invented example, not a real client).

Read more

In-process analytics in 2026: revisit DuckDB, Polars and chDB before you spin up another warehouse for a small question

Think of a knitwear exporter in Tiruppur that makes T-shirts, polos and kids’ wear for buyers in Europe, the US, the Gulf and Japan (an invented example, not a real client).

Read more

Time series databases in 2026: revisit TimescaleDB, QuestDB and InfluxDB 3 Core before you store another sensor reading

Take a cold-chain company in Nashik that runs pre-cooling and cold storage chambers for export grapes and onions at four sites: MIDC Ambad, Pimpalgaon Baswant, Dindori and Niphad (an invented example, not a real client).

Read more