SQLite in production: patterns that have aged well
Table of contents
- Key takeaways
- WAL mode changes everything
- Litestream and replication was the turning point
- Loads where SQLite is still the best choice
- Where SQLite is still a bad idea
- When it pays off
- Conclusion
- Frequently asked questions
- How many users and how large a database can SQLite handle on a web server?
- How do I back up a production SQLite database without stopping writes?
- Can I share one SQLite file between several application servers?
- Sources
SQLite in production is a sound choice for small and mid-sized web applications once WAL mode is enabled, since concurrency on typical web loads improves by roughly two orders of magnitude. Litestream streams the WAL to S3-compatible object storage for point-in-time restore, and NVMe disks on cheap VPS plans removed the old disk objection.
SQLite has been the world’s most deployed database for twenty-five years, but for most of that time it was dismissed as a toy option for servers. That perception has shifted in the last three years thanks to three forces: mature WAL, NVMe disks universal on even cheap VPS plans, and projects like Litestream and libSQL. Those projects solved the two classic objections, lack of replication and lack of remote access. This post collects what still works and what doesn’t after several years of watching it run in production.
Key takeaways
-
WAL mode changes SQLite so profoundly that talking about SQLite-without-WAL and SQLite-with-WAL as the same database is misleading: the concurrency difference is a hundredfold on typical web loads.
-
Litestream streams the WAL to an S3-compatible object store in near real time, enabling point-in-time restore with at most a few seconds of data loss.
-
libSQL, the SQLite fork Turso maintains, adds embedded synchronized replication for geographically distributed architectures.
-
SQLite covers most web applications with fewer than a thousand concurrent users and databases under 100 GB.
-
Doesn’t apply when the load has intensive concurrent writes, multi-server file access is needed, or advanced Postgres extensions are required.
WAL mode changes everything
The gap between SQLite in the default rollback journal mode and SQLite with WAL enabled is so wide that talking about both as the same database is misleading. Rollback mode serializes writes and reads on a single file lock, which caps useful concurrency at a handful of queries per second. WAL mode, a forward-write log kept in a separate file, allows concurrent reads while a write is active, and restricts collisions to simultaneous writers, as documented by the SQLite project itself[1]. For web apps with a 95 to 5 read-to-write ratio, the performance difference is a hundredfold.
Enabling WAL is a single statement at database-open time, but it comes with details worth knowing:
-
The WAL file grows while transactions are active and is checkpointed automatically once it reaches a threshold (1,000 pages by default, per the SQLite documentation). On workloads with frequent writes it is worth raising that threshold.
-
The WAL file and the main file must share a filesystem; trying to put them on different volumes is a classic mistake that breaks atomicity.
The second lever for server performance is tuning page size and cache. SQLite defaults to 4 KB pages, which fits most workloads well. For tables with small rows and heavy random reads, moving to 8 KB or 16 KB pages at database-creation time can improve filesystem-cache efficiency.
Litestream and replication was the turning point
The historical Achilles’ heel of SQLite on the server was backup. Until Litestream[2] arrived in 2021, the only decent way to back up a live database was to stop writes, copy the file, and resume, which is not viable on any system with real traffic.
Litestream solved the problem by streaming the WAL to an S3-compatible object store in near real time, with the agent’s default sync interval at one second. That enables point-in-time restore with at most a few seconds of data loss. I cover the full mechanics in Litestream: near-real-time replication for SQLite.
What started as a clever trick has become the piece that makes serious production SQLite viable. In deployed projects, pointing Litestream at an S3-compatible bucket such as Hetzner Object Storage or Cloudflare R2 costs a few cents a month in storage and protects against disk failure or server destruction. Restoring is one command: it downloads the bucket state, rebuilds the database to the chosen instant, and starts the application.
The most recent evolution is libSQL, the SQLite fork Turso maintains and which already powers Turso Cloud in production. It added embedded synchronized replication, allowing a primary server database and local replicas on each client or in regional nodes. For geographically distributed applications, libSQL solves local reads without running multi-region Postgres.
Don’t confuse it with Turso Database (formerly known as Limbo), the from-scratch Rust rewrite of SQLite the same company maintains on GitHub[3]. As of mid-2026 it is still in beta on its way to a first stable release, per Turso’s own blog[4]. For production today libSQL, not the rewrite, remains the mature choice.
Loads where SQLite is still the best choice
SQLite on server with WAL and Litestream covers without blinking most web applications with fewer than a thousand concurrent users and databases under 100 GB. That includes management systems, internal dashboards, sites with moderate dynamic content, and mid-size APIs. In practice, anything that isn’t a store with massive traffic spikes or a social network with heavy writes.
The most visible success story is sqlite.org itself, which, per the project’s own documentation[5], serves its entire site from a SQLite database. It handles 400,000 to 500,000 HTTP requests a day, 15-20% of them dynamic, running on a single VM that shares a physical server with 23 others.
The second niche where it shines is development and testing. Being able to swap Postgres for SQLite in the continuous-integration environment cuts test startup time from tens of seconds to under two. Most of standard SQL is compatible, and when something diverges it shows up early.
The third space is desktop and mobile applications that sync with the cloud. With libSQL and its bidirectional sync, the pattern of a local client database that replicates with a central server has gotten drastically simpler.
Where SQLite is still a bad idea
There are cases where SQLite on the server doesn’t pay off, and it’s worth being honest about them:
-
Sustained intensive concurrent write workload: SQLite serializes writers by design. A system with hundreds of write transactions per second will hit a ceiling. Postgres, MariaDB, or even MySQL with InnoDB can absorb ten to a hundred times more concurrent writes.
-
Multi-process architecture with more than one application server accessing the same file: SQLite isn’t built for over-the-network file access. Locking over NFS is unreliable and has been the source of corruptions in real projects. If your architecture needs more than one server reading and writing against the same database, SQLite is not the right tool, period.
-
Needing advanced Postgres features: serious geospatial extensions, production-grade full-text search, or two-phase transactional support. The gap with Postgres in these areas is substantial.
-
Mostly analytical workload over large volumes: if what’s behind it is aggregation and columnar scans rather than row-level transactions, the right engine is another one. I cover that crossover in SQLite and DuckDB: when each is the right choice.
The same logic of picking the right tool for the right load applies to the Citus for scaling Postgres horizontally debate: every database has its own range of strengths.
When it pays off
The question is no longer whether SQLite can sustain a web server, because it can. The question is whether your concrete case fits its strength profile. If your application:
-
Reads much more than it writes.
-
Fits on a single server.
-
Doesn’t need synchronous multi-node replication.
-
Values the operational simplicity of a file with no server process.
Then SQLite with WAL and Litestream is probably the lowest total-cost option in engineering and operations.
If instead your system will grow to more than one server, will have heavy concurrent writes, or you already have Postgres running without pain, there’s no reason to switch. The beauty of SQLite in 2025 isn’t that it replaced Postgres, but that it stopped being an eccentric option for a certain range of problems. That range is larger than SQLite’s reputation suggests.
Conclusion
SQLite with WAL and Litestream can save weeks of infrastructure assembly that adds nothing to a service with 200 monthly active users. Taking it seriously before assuming you need a separate database server is part of practical engineering.
Also in Spanish: SQLite en producción: patrones que han envejecido bien.
Frequently asked questions
How many users and how large a database can SQLite handle on a web server?
With WAL mode and Litestream it covers most web applications with fewer than a thousand concurrent users and databases under 100 GB. That includes management systems, internal dashboards, sites with moderate dynamic content and mid-size APIs. The sqlite.org project itself serves its entire site from a SQLite database. It handles 400,000 to 500,000 HTTP requests a day, 15-20% of them dynamic, on a single VM that shares a physical server with 23 others.
How do I back up a production SQLite database without stopping writes?
With Litestream, which streams the WAL to an S3-compatible object store in near real time: the agent's default sync interval is one second. It enables point-in-time restore with at most a few seconds of data loss. Pointing it at a bucket such as Hetzner Object Storage or Cloudflare R2 costs a few cents a month. Restoring is one command that downloads the bucket state and rebuilds the database to the chosen instant.
Can I share one SQLite file between several application servers?
No: SQLite isn't built for over-the-network file access. Locking over NFS is unreliable and has been the source of corruptions in real projects. If you need more than one server reading and writing against the same database, SQLite is not the right tool. For local reads in geographically distributed architectures, libSQL (the fork Turso maintains) adds embedded replication with a primary server database and replicas in regional nodes or on each client.