SQLite on the Server: Turso, Litestream, and LiteFS
For most of its life, SQLite was the database you embedded in a phone app or a desktop tool, never the one behind a web service. That assumption is now wrong. A stack of tooling — Litestream, LiteFS, and Turso — has turned SQLite into a legitimate server database with backup, replication, and edge distribution, while keeping its defining advantage: the database is a local file, so queries run in-process with no network round trip. Running SQLite on the server means your read path can be microseconds instead of milliseconds, because there's nothing to cross the wire.
I'll be upfront that I like this pattern more than I expected to. I've shipped a read-heavy internal service on LiteFS and the P99 latency chart was almost boring — flat and low — precisely because the "database call" was a local memory-mapped read. But the model has real constraints, and pretending otherwise gets you burned. Here's the honest picture.
Why locality wins
The dominant cost in a typical web request against a networked database isn't the query — it's the round trip. Even inside one datacenter, a Postgres query pays connection acquisition, network latency, serialization, and result transfer. Do that three or four times per request and you've spent milliseconds on transport alone.
SQLite collapses that to a function call. The database file is memory-mapped into your process; a SELECT on an indexed column returns in microseconds. There's no connection pooling for serverless databases to configure, no max_connections to exhaust, no pooler tier to run — the whole category of connection-management problems simply doesn't exist because there's no connection. For read-heavy workloads where the working set fits on a node, this locality routinely beats a "better" database engine sitting behind a network hop. That's the core insight the whole ecosystem is built on.
The catch, and it's a big one: SQLite is single-writer. It serializes writes with a lock, so it's superb at concurrent reads and deliberately modest at concurrent writes. That single fact shapes every architecture decision below.
Litestream: backup without a replica
The simplest step past "a file on one server" is Litestream. It watches SQLite's write-ahead log and continuously streams changes to object storage (S3, GCS, etc.), giving you continuous backup and point-in-time recovery. The database stays single-node; Litestream just makes losing the node non-catastrophic.
# litestream.yml
dbs:
- path: /data/app.db
replicas:
- type: s3
bucket: my-app-backups
path: app.db
retention: 72h
sync-interval: 1s
With sync-interval: 1s your recovery point objective is about a second — if the box dies, you restore from S3 and lose at most the last second of writes. This is the right choice when a single node has enough capacity, you want disaster recovery, and you don't need read replicas. It's remarkably little machinery for real durability.
LiteFS: actual multi-node replication
When one node isn't enough — you want read replicas near users, or failover — LiteFS steps up. It's a FUSE filesystem that intercepts SQLite's file operations and replicates the database across a cluster with a single primary for writes and multiple read replicas. Writes go to the primary; reads can be served locally on any node.
This is where the single-writer nature becomes an architectural rule you design around: replicas are read-only, and writes must be forwarded to the primary. LiteFS handles the forwarding, but your app has to tolerate the model — reads on a replica may be slightly stale relative to the primary (replication lag), and write latency depends on distance to the primary. For a read-heavy app with a clear write path, that's a fine trade. For a write-heavy, globally-distributed system, it's a poor fit, and you should reach for a database built for distributed writes instead.
LiteFS pairs naturally with edge computing functions: put read replicas in the same regions as your edge compute and reads become local everywhere, while writes funnel to a single primary region.
Turso and libSQL: the managed edge option
Turso takes the idea furthest. It's a managed platform built on libSQL, an open-source fork of SQLite that adds server-friendly features the upstream project doesn't ship — a remote client protocol, embedded replicas, and native replication. Instead of running Litestream or LiteFS yourself, you get distributed replicas placed near your users and a client SDK that talks to them.
import { createClient } from "@libsql/client";
// Embedded replica: local file synced from a remote primary
const db = createClient({
url: "file:local.db",
syncUrl: "libsql://my-db.turso.io",
authToken: process.env.TURSO_TOKEN,
});
await db.sync(); // pull latest from the primary
const rows = await db.execute("select id, email from users where id = ?", [42]);
The embedded-replica mode is the clever part: reads hit a local file (microseconds), while a background sync keeps it current with the remote primary. You get edge-local read latency and managed replication without operating the plumbing. The cost is a dependency on a managed platform and libSQL's divergence from stock SQLite — usually fine, occasionally something to check when a tool assumes vanilla SQLite.
Choosing among the three
They aren't really competitors; they occupy different rungs of the same ladder:
| Tool | Adds | Multi-node reads | Managed |
|---|---|---|---|
| Litestream | Continuous backup to object storage | No | Self-hosted |
| LiteFS | Primary/replica cluster replication | Yes (read replicas) | Self-hosted (Fly-friendly) |
| Turso / libSQL | Edge distribution + remote protocol | Yes (edge replicas) | Managed |
My decision tree is short. Single node, just want durability? Litestream. Need read replicas or failover and happy to run infrastructure? LiteFS. Want edge-local reads with someone else operating it? Turso. All three assume a read-heavy, single-writer-friendly workload — that's the boundary condition for the whole approach.
When not to do this
Be honest about the failure cases. If your application is write-heavy with high concurrent write contention, SQLite's single-writer lock becomes the bottleneck and no amount of replication helps — writes still serialize through one node. If you need multiple services writing to the same database independently, the embedded model fights you. And if your dataset genuinely doesn't fit comfortably on a single node's disk, you've outgrown the pattern.
Within its lane, though, server-side SQLite is one of the best price/performance/simplicity trades in modern backend engineering. Fewer moving parts, dramatically lower read latency, and durability that used to require a whole database tier. I now treat "could this just be SQLite?" as a real question at the start of read-heavy projects — often the answer is yes, and the resulting system is smaller and faster than the Postgres-by-default alternative.
Resources
- SQLite: Appropriate uses for SQLite
- Litestream documentation
- LiteFS documentation (Fly.io)
- Turso documentation
- libSQL repository
- SQLite WAL mode documentation
Trade-offs I keep revisiting for sqlite on the server
Operating sqlite on the server well means tying design choices to measurable outcomes and explicit owners. Ambiguous ownership is how pages rot.
For sqlite on the server:
- Write the SLO and the user journey it protects
- Automate the boring verification; reserve humans for judgment calls
- Prefer progressive delivery with fast rollback over big-bang cuts
- Keep runbooks next to the code that can break
Revisit the design when the metric that justified sqlite on the server stops moving — sunsetting is a feature.
| Signal | Target | Alarm |
|---|---|---|
| Coverage % | Team-defined SLO | Page on burn rate |
| Mean time to detect | Baseline − noise | Ticket if sustained |
| Escapes to prod | Budget cap | Weekly review |
What reviewers should challenge in sqlite on the server PRs
Reviewers should challenge assumptions encoded in sqlite on the server: defaults copied from tutorials, timeouts that exceed upstream SLAs, and authz checks applied only on the primary UI path. Require a short threat or failure note in the PR when the change touches a trust boundary.
Concrete probes:
- Scenario B for sqlite on the server: bad config shipped — prove rollback within the declared RTO without data corruption.
- Scenario C for sqlite on the server: traffic 3× baseline — prove autoscaling or shedding keeps the golden journey healthy.
- Scenario A for sqlite on the server: partial dependency outage — prove clients degrade gracefully and retries do not amplify load.
Post-incident changes after sqlite on the server failures
Roll out sqlite on the server behind a flag or weighted route when possible. Start with internal users or a low-risk geography. Watch the signals in the table for at least one full business cycle before calling the migration done. Keep the previous path warm until error budgets stabilize.
Document the owner, the dashboard, and the single command that reverts the change. If that sentence is hard to write, the design is not ready for production traffic.
Multi-tenant concerns in sqlite on the server
Detail 1 (489): for sqlite on the server, define the contract between producers and consumers explicitly — payload shape, timeout, and idempotency key. When multi-tenant concerns in sqlite on the server becomes painful, it is usually because that contract was implicit.
I keep a short matrix: who can break sqlite on the server, how we detect it within five minutes, and who is paged. Update the matrix when ownership moves. Add one synthetic check that exercises the failure path, not only the happy path. Prefer checks that run continuously over quarterly manual reviews that everyone skips under deadline pressure.
If you only remember one thing about sqlite on the server: optimize for reversible decisions. Reversibility beats cleverness when the incident channel is busy and the blast radius is unclear.
Compliance evidence for sqlite on the server
Detail 2 (388): for sqlite on the server, define the contract between producers and consumers explicitly — payload shape, timeout, and idempotency key. When compliance evidence for sqlite on the server becomes painful, it is usually because that contract was implicit.
I keep a short matrix: who can break sqlite on the server, how we detect it within five minutes, and who is paged. Update the matrix when ownership moves. Add one synthetic check that exercises the failure path, not only the happy path. Prefer checks that run continuously over quarterly manual reviews that everyone skips under deadline pressure.
If you only remember one thing about sqlite on the server: optimize for reversible decisions. Reversibility beats cleverness when the incident channel is busy and the blast radius is unclear.
Frequently asked questions
Can SQLite be used as a server database?
Yes, with the right tooling. SQLite is an embedded library rather than a client-server database, so historically it wasn't a server backend. Projects like Litestream, LiteFS, and Turso add replication, backup, and distribution on top of SQLite, letting it serve production web applications — often with lower latency than a networked database because queries execute in-process against a local file rather than over the network.
What is the difference between Litestream, LiteFS, and Turso?
Litestream continuously streams SQLite's WAL to object storage for point-in-time backup and disaster recovery, but the database stays single-node. LiteFS is a FUSE filesystem that replicates SQLite across nodes with a single primary for writes and multiple read replicas. Turso is a managed platform built on libSQL, a SQLite fork, that distributes replicas to the edge and offers a remote client protocol. They solve backup, multi-node replication, and managed edge distribution respectively.
Why is SQLite fast for server workloads?
Because there is no network hop between the application and the database. Queries run in-process against a memory-mapped local file, so a read that would cost a network round trip against Postgres costs microseconds against local SQLite. For read-heavy workloads where data fits on a node, this locality often beats a networked database despite Postgres having a more sophisticated engine.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →