SQLite on the Server: Turso, Litestream, and LiteFS

BackendDatabasesEdge

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

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 →