SQLite Is Eating the World from the Edge Inward
SQLite is probably running on your phone right now. And your laptop, your browser, your TV, your car. The project’s own conservative estimate puts over one trillion SQLite databases in active use, more than all other database engines combined. It’s the second most widely deployed software library in existence, behind only zlib. Every Android device, every iPhone, every Windows 10 and 11 installation, every copy of Chrome, Firefox, and Safari ships with it. The Library of Congress considers it an official archival format, which is a hell of a thing for a C library that compiles to about 750KB.
And until recently, the consensus in web development was that SQLite was fine for phones and browsers and unit tests, but if you were building a real application with real users, you needed a “real” database. Postgres, MySQL, maybe CockroachDB or PlanetScale if you were feeling fashionable. Something with a network protocol, a connection pool, a replication story, and a monthly invoice.
That consensus is collapsing. The people who spent years building infrastructure to avoid SQLite are having an uncomfortable realization: for an enormous class of applications, they never needed any of it.
How we got here
SQLite’s rise in production is really two stories. One about what changed technically. The other about what changed in how we think about architecture.
On the technical side, a handful of tools converged between 2022 and 2025 to solve SQLite’s two genuine limitations: no replication, and no concurrent writes. A single-process embedded database that writes to a local file has an obvious failure mode (lose the machine, lose the data) and an obvious scalability ceiling (only one writer at a time).
Ben Johnson’s Litestream, built at Fly.io, was the first shot across the bow. Litestream works by hijacking SQLite’s WAL (Write-Ahead Log) mode. In WAL mode, SQLite appends writes to a separate log file and periodically checkpoints them back into the main database. Litestream steps into this process: it holds a long-lived read transaction to prevent automatic checkpoints, captures the WAL pages as they’re written, and streams them to S3, Azure, Backblaze, or any other object storage. Your application talks to standard SQLite through standard drivers. Litestream is invisible to it. You get continuous replication to cloud storage for essentially free.
# That's the whole replication setup
litestream replicate myapp.db s3://my-bucket/myapp.db
Then LiteFS came along from the same Fly.io orbit, a FUSE filesystem that presents a replicated SQLite database as a local file. Your app opens it, reads it, writes to it, and LiteFS handles the replication transparently. Each region gets a local copy of the database, LiteFS handles sync between regions, and your app thinks it’s talking to a local file on a local disk.
On the platform side, Cloudflare D1 reached general availability in April 2024. D1 runs SQLite at the edge, literally inside Cloudflare’s V8 isolates spread across 300+ locations worldwide. Each D1 database is a single-file SQLite database. You query it from a Cloudflare Worker with SQL, and the query runs in the same process as your code. No network hop, no connection pool, no protocol negotiation. The query latency is roughly the same as a function call, because that’s what it is.
Turso took a different but complementary approach with libSQL, an open-source fork of SQLite that adds the features needed for networked deployment: client/server mode, embedded replicas that sync with a remote primary, and a platform that manages the replication for you. You get a local SQLite file embedded in your application and transparent synchronization with Turso’s infrastructure. Reads from the local embedded replica hit local disk. Writes sync to the primary asynchronously. The latency characteristics are absurd.
The performance argument nobody makes
Most comparisons miss the obvious thing about SQLite: it’s fast. Not “fast for an embedded database.” Fast.
When you query Postgres, your application opens a TCP connection (or borrows one from a pool), serializes the SQL query into the Postgres wire protocol, sends it over the network, the Postgres server parses and plans the query, executes it against its buffer pool, serializes the results back into the wire protocol, sends them over the network, and your application deserializes them. For a simple SELECT * FROM users WHERE id = 42, the actual data work might take 0.1ms. The round-trip and protocol overhead can easily add 1-5ms on top of that. On a cold connection, it’s worse. On a busy server under contention, it’s worse still.
When you query SQLite, the query travels through a C function call. The SQLite library parses the SQL, walks the B-tree, finds the row, and returns it. Same 0.1ms of data work. Same total latency. No network hop, no protocol, no serialization, no connection pool contention. The “overhead” is a function call.
The Braintrust team ran benchmarks comparing SQLite to Postgres and found SQLite was 4.9x faster on reads for their workload. Not because SQLite’s query engine is five times better. Postgres’s query planner is substantially more sophisticated. But the overhead of a network protocol wipes out that advantage for the majority of queries that don’t need a sophisticated planner.
SQLite’s own documentation has a page titled “Faster Than The Filesystem” that explains how it can outperform raw fread() calls for many workloads by doing large sequential reads and intelligent caching. The database is literally faster than opening a file. This isn’t a secret. It just doesn’t fit the narrative that embedded databases are toys.
The latency hiding in plain sight
Why doesn’t SQLite’s performance advantage get more attention? Because we’ve been trained not to think about the latency we spend on database protocols. A 2ms Postgres round trip feels “fast” when you’re comparing it to the 200ms HTTP requests users see. But at the application layer, that 2ms is real, and it compounds.
Consider a page render that runs five queries: a user lookup, a permissions check, a settings fetch, a content query, and a related items query. Against Postgres, that’s five round trips, each adding 1-3ms of pure protocol overhead. Against SQLite, that’s five function calls that collectively take less than 1ms of overhead. The data work is identical. The latency difference comes from SQLite not needing to prove its existence by sending your data over a TCP socket.
At the edge, this difference becomes architectural. When your code and your database share a process, you don’t just save round-trip time. You eliminate the operational surface area of a database server. No connection pooling to configure, no PgBouncer to tune, no max_connections to adjust, no network firewall rules, no TLS certificates to rotate for database connections, no failover to configure. The database is your application. Literally.
The “but concurrency” objection
The first objection to any SQLite-in-production pitch is always the same: “but only one writer at a time.” This is true and it matters, but the question is whether it matters for your application.
SQLite in WAL mode allows concurrent reads and a single concurrent writer. Readers don’t block writers, writers don’t block readers. The limitation is that you can only have one writer transaction active at any given moment. If two writers try simultaneously, one waits.
For a single application server talking to its own local SQLite database, this is almost never a problem. Your application is the only writer. It processes one request at a time per process, so writes are naturally serialized. Even with async I/O where multiple requests overlap, SQLite’s write lock is held for microseconds at a time for most operations. The contention window is so small that concurrent writes are rarely noticed.
The trouble starts with multi-instance deployments. If you have five application servers all trying to write to the same SQLite file over NFS, you’re going to have a bad time. But that’s a terrible architecture regardless of your database choice, and it’s not what SQLite-in-production advocates recommend.
The recommended pattern is one database per instance, with replication between instances. This is exactly what LiteFS, Litestream, and Turso all implement. Each application server has its own local SQLite database. Writes go to the local copy. The replication layer syncs changes to other instances. You just don’t need Postgres to do it.
Cloudflare D1 takes a different approach: each database is single-threaded and single-writer, but you can have thousands of databases. Their docs recommend horizontal scaling across smaller databases (per-user, per-tenant, or per-entity) rather than vertical scaling within one big database. The pricing model supports this: you pay per query and per GB stored, not per database. Having 10,000 databases costs the same as having one, if the total query volume and storage are the same.
The cognitive cost nobody budgets for
I think the real force behind SQLite’s resurgence is something the industry is bad at quantifying: operational simplicity.
A Postgres deployment, even a modest one, comes with a long tail of operational knowledge. Connection pooling configuration. Vacuum strategies. Index bloat monitoring. Replication slot management. WAL retention. Autovacuum tuning. shared_buffers, work_mem, effective_cache_size, maintenance_work_mem. The Postgres configuration guide has over 300 parameters. You don’t need to understand all of them, but you need to understand enough to keep your application running, and the “enough” threshold keeps rising as your data grows.
SQLite has about 30 compile-time options, and you almost never need to touch any of them. It ships with sensible defaults that work for 99% of workloads. The “tuning” section of the SQLite documentation fits on a single page. There’s no background process to manage, no daemon to restart, no config file to rotate. The database is a file. You back it up by copying the file. You move it by copying the file. You version it by copying the file. Your existing Unix skills apply.
Every piece of infrastructure you add is a thing that can go wrong in production at 3 AM. Every background process is a memory leak waiting to happen, a file descriptor that can exhaust, a log that can fill a disk. SQLite’s operational surface area is almost nonexistent, and that frees you to spend your complexity budget on things that actually matter.
What SQLite is not
SQLite doesn’t replace Postgres. It replaces the default assumption that every application needs Postgres.
Postgres has real advantages that SQLite cannot match. ACID transactions across concurrent writers from multiple clients. Query planning for complex analytical queries. Full-text search with ranking. JSONB with GIN indexes. Extensions for geospatial data, time series, and specialized workloads. Row-level security. Listen/notify. These features matter when you need them.
But the pattern I keep seeing: teams choose Postgres because they might need these features someday, not because they need them today. They stand up a Postgres instance for an application that has three tables and a few hundred users. They add PgBouncer because they read somewhere that you should. They configure a monitoring dashboard because their database is now infrastructure that needs monitoring. They’re spending time and money maintaining a data tier for a workload that could run on a single file.
SQLite forces a useful discipline. If your application doesn’t have concurrent writers, it doesn’t pay the cost of concurrent-write infrastructure. If your data fits in memory, it doesn’t pay the cost of disk-optimized architectures. If your queries are simple, it doesn’t pay the cost of a complex query planner. You scale what you need. You don’t pre-pay for capabilities you haven’t used.
The edge is just the beginning
SQLite is having its moment now because of edge computing. When your application code runs in a V8 isolate at 300 edge locations, the last thing you want is a network round trip to a database server in Virginia. You want a database that runs in the same process, in the same location, with the same latency characteristics as a local variable.
Cloudflare D1, Turso, and Neon’s serverless Postgres all recognized this. D1 and Turso bet on SQLite, and the bet is paying off because the performance characteristics of an embedded database line up with the edge computing model. You avoid the network hop to your database, sure. But you also avoid the conceptual hop. Your code and your data live in the same process. The mental model collapses to something trivially simple.
The less discussed direction is the reverse: not edge to cloud, but local to edge. SQLite has always been the database of the client. Every phone, every browser, every desktop app ships with it. The new tools (Litestream, LiteFS, libSQL, Turso) create a bridge between the local SQLite databases that already exist on billions of devices and the server-side SQLite databases proliferating at the edge. Same database format, same query language, same transaction semantics, from a phone’s local storage to a Cloudflare Worker in Tokyo to a Fly.io machine in Frankfurt. The replication layer handles the rest.
That’s new. Not a better version of something we already had. A different model for where data lives and how it moves.
The bitter lesson, database edition
There’s a parallel with the broader trajectory of software. We built enormous distributed systems in the 2010s because the problems demanded it. Companies like Uber and Netflix needed to manage petabytes of data across thousands of nodes. The tooling that emerged (Kubernetes, CockroachDB, Kafka, and their ilk) is impressive work.
Most companies aren’t Uber or Netflix. Most applications serve thousands of users, not millions. Most datasets fit in a few gigabytes. Most query patterns are simple reads and writes against well-indexed tables. For these applications, distributed infrastructure isn’t enabling scale. It’s imposing tax.
SQLite’s resurgence is a correction, not a revolution. The default should be the simplest thing that works. Start with a file. If you outgrow it, you’ll know, and the migration path from SQLite to Postgres is well-trodden. But starting with Postgres because you might outgrow SQLite is like buying a forklift because you might get a piano.
The industry spent ten years building bigger distributed databases. Now it’s building better ways to make the simplest database work everywhere. I know which direction I find more interesting.