SQLite in Production — From "That's Crazy" to Turso, LiteFS, and Cloudflare D1

Remember when saying “we use SQLite in production” would get you laughed out of a system design interview? That era is definitively over. The past two years have seen an explosion of production-grade tools built on top of SQLite, and the results are compelling enough that we migrated one of our services from Postgres — with dramatic results.

The New SQLite Ecosystem

Let me lay out what’s happened:

  • Turso — A distributed SQLite platform built on their libSQL fork. They’ve added features that SQLite core has historically refused: ALTER COLUMN, native replication across regions, and recently, vector search support. You get a SQLite-compatible database that replicates globally with edge read replicas.
  • LiteFSFly.io’s answer to SQLite replication. It uses FUSE to intercept filesystem operations on the SQLite file and replicate WAL frames to other nodes. Your app thinks it’s writing to a normal SQLite file, but the data is being replicated across Fly’s edge network.
  • Cloudflare D1 — SQLite running on Cloudflare’s edge network. This one is significant because it answers the question “how do you put a database on every edge node?” You can’t run Postgres on 300+ edge locations, but you can absolutely run SQLite.
  • Litestream — Streaming replication of SQLite to S3, Azure Blob, GCS, or SFTP. It continuously streams WAL changes to object storage, giving you point-in-time recovery without any application changes.

Why the Numbers Are Compelling

The performance characteristics of SQLite, when colocated with your application, are genuinely hard to beat:

  • 100K+ read queries per second on modern hardware. This isn’t a benchmark number — it’s what you see in production with a well-indexed database.
  • Sub-millisecond read latency. No network hop means your database queries are essentially memory operations (assuming the working set fits in the OS page cache). Compare this to Postgres where even on the same machine you’re paying ~1ms for the Unix socket roundtrip.
  • Zero connection management. No connection pools, no max_connections tuning, no “too many connections” errors during traffic spikes, no pgBouncer configuration. The database is a function call, not a network service.

The DHA (Durable Hosting Architecture) concept from the SQLite team captures this well: instead of separating your application server and database server, you colocate them. The database file lives on the same machine as the app. Reads are local I/O. Writes go to a local file that’s replicated asynchronously for durability.

Our Migration Story

We migrated a read-heavy API service from Postgres (hosted on RDS) to Turso last quarter. The service handles product catalog queries — lots of reads, infrequent writes, data that changes maybe a few hundred times per day.

The results:

Metric Postgres (RDS) Turso (SQLite)
P50 latency 12ms 0.8ms
P99 latency 45ms 3ms
Monthly infra cost $340 $95
Connection errors/week ~15 0
Deployment complexity High (migrations, connection strings, secrets) Low (embedded database, single binary)

The 70% cost reduction came from eliminating the RDS instance entirely. The latency improvement came from removing the network hop — the SQLite database is literally embedded in the application process.

But the biggest win was operational simplicity. No more RDS maintenance windows. No more connection pool debugging at 2 AM. No more “the database is in us-east-1 but the app is in us-west-2” latency discussions. The database goes where the app goes.

Where SQLite Still Falls Short

I want to be honest about the limitations, because I’ve seen too many “SQLite is all you need” takes that gloss over real constraints:

  • Write throughput is limited. SQLite’s WAL mode allows concurrent reads with a single writer, but writes are still serialized. If you need more than ~1,000 write transactions per second, you’ll hit contention. For write-heavy workloads (event logging, real-time analytics ingestion, high-traffic social feeds), Postgres or a purpose-built solution is still the right choice.
  • No cross-database joins. If your architecture involves multiple services that need to query each other’s data, you can’t do a cross-database join in SQLite. You need application-level orchestration.
  • Immature monitoring tooling. The ecosystem of monitoring, alerting, and observability tools for SQLite in production is thin compared to Postgres. No pganalyze equivalent, no mature query performance monitoring, limited slow query logging.
  • The type system is permissive. SQLite’s dynamic typing means it’ll happily store a string in an INTEGER column. If you’re coming from Postgres with strict typing, you need to add application-level validation or use strict mode (added in SQLite 3.37).

The Question

Is SQLite-in-production a real paradigm shift for how we build web applications, or does it only make sense for a specific slice of use cases — read-heavy, moderate data size, single-region deployments?

I’m particularly curious to hear from anyone running SQLite at scale with Turso or LiteFS. How’s the multi-region replication story in practice? And for the data engineers — how are you thinking about SQLite as an analytical store alongside tools like DuckDB?

I migrated my side project from Supabase (Postgres) to Turso last month and the DX improvement was incredible. Let me share the specifics because the difference really is stark.

What got better immediately:

  • No more connection string management. With Supabase, I had a connection string, a pooler connection string (for serverless), and an API key. With Turso, I have a database URL and a token. That’s it.
  • No more “too many connections” errors. My app runs on Vercel serverless functions, and every cold start used to mean a new Postgres connection. I was constantly hitting the connection limit during traffic spikes, even with pgBouncer. With Turso’s embedded replicas, the app reads from a local SQLite file — no connections at all for reads.
  • No more cold start latency from database connections. Establishing a Postgres connection takes 50-100ms (with TLS). That’s gone. The database is local.
  • Deployment is trivial. turso db create, set two environment variables, done. No provisioning, no VPC peering, no security groups.

My P95 API response time went from ~180ms (Vercel → Supabase Postgres in another region) to ~25ms (Vercel → local embedded replica). That’s a transformative difference in how the app feels.

What bit me:

The one significant issue was SQLite’s type system. Postgres would reject INSERT INTO users (age) VALUES ('not a number') with a type error. SQLite happily stores it. I had a bug where a form field was sending a string instead of an integer, and it went undetected for two weeks because SQLite didn’t complain.

I ended up adding Zod validation at the API layer for every database input — something I didn’t need with Postgres’s strict typing. It’s arguably better practice anyway (validate at the boundary), but it was extra work I hadn’t anticipated.

The verdict:

For small-to-medium apps — especially those running on serverless platforms — this is genuinely a game changer. The elimination of connection management alone is worth it. For anything with complex relational queries, extensive use of Postgres-specific features (PostGIS, full-text search with tsvector, JSONB operators), or heavy write throughput, I’d still reach for Postgres without hesitation.

The operational simplicity is the killer feature here, and I want to underscore this from an infrastructure perspective because the difference is massive.

Running Postgres in production requires managing:

  • Connection pooling (pgBouncer or PgCat configuration, monitoring pool exhaustion)
  • Vacuum schedules (autovacuum tuning, monitoring bloat, occasional manual VACUUM FULL on large tables)
  • Replication lag monitoring (streaming replication to read replicas, failover procedures)
  • Backup verification (pg_dump or WAL archiving, actually testing restores regularly — which most teams don’t)
  • Version upgrades (major version upgrades are still painful, require dump/restore or pg_upgrade with downtime)
  • Extension compatibility (pgvector, PostGIS, etc. need to be compatible with your Postgres version)
  • SSL certificate management, pg_hba.conf rules, monitoring for idle connections eating memory

That’s a lot of operational surface area, and most of it is invisible until something breaks at 3 AM.

With SQLite + Litestream, the operational model is:

  1. A single file on disk
  2. WAL changes stream to S3 continuously
  3. Disaster recovery = download the file from S3 and start the app
  4. Backups = the S3 bucket with versioning enabled
  5. “Replication” = deploy another copy of the app and let it sync

I’ve run this setup for 8 months on a production service and the only maintenance I’ve done is rotating the S3 credentials once. Compare that to the weekly Postgres care-and-feeding at my previous job.

However, I have a serious concern:

I worry about teams adopting SQLite in production without fully understanding the write concurrency model. SQLite’s WAL mode is great — it allows concurrent readers while a single writer holds the lock. But writes are still serialized. Every write transaction acquires an exclusive lock on the database.

For most web apps doing a few hundred writes per second, this is completely fine. But I’ve already seen two teams hit this wall:

  1. A team tried to use SQLite for a real-time chat app. Under load testing, they hit ~800 messages/second and suddenly write latency spiked from 1ms to 500ms because transactions were queuing behind the write lock. They didn’t understand it was a file locking issue, not a “slow query” issue.

  2. Another team used SQLite for session storage. During a traffic spike, thousands of concurrent session writes caused SQLITE_BUSY errors that crashed their Node.js process because they weren’t handling the busy timeout correctly.

If you’re considering SQLite in production: benchmark your write workload first. Set busy_timeout appropriately. And if your writes exceed ~1,000 TPS sustained, look at Postgres, CockroachDB, or a purpose-built solution instead. SQLite’s strength is simplicity for the 90% of apps that are read-heavy. Don’t force it into the write-heavy 10%.

The analytics story is where SQLite gets really interesting, and I think it’s underappreciated in these discussions that tend to focus on OLTP use cases.

DuckDB proved that embedded analytical databases are viable. It showed that you don’t need a Spark cluster or a Snowflake warehouse to run analytical queries on millions of rows — an embedded engine running on your laptop can do it in seconds. SQLite is now following a similar trajectory for lighter analytical workloads, thanks to a growing ecosystem of extensions.

What’s available now:

  • sqlite-vec brings vector search to SQLite. I’m using it for a small semantic search feature — storing embeddings alongside the source data in the same database. No need for a separate Pinecone or Weaviate instance. For datasets under a few million vectors, the performance is more than adequate.
  • JSON extensions (built into modern SQLite) make it competitive for semi-structured data analysis. json_extract, json_each, json_tree — you can query nested JSON documents with SQL. It’s not as elegant as Postgres’s JSONB operators, but it works.
  • sqlite-http lets you make HTTP requests from SQL queries, which sounds wild but is surprisingly useful for enrichment pipelines.

My current setup:

I’m running a small data pipeline for a client where the intermediate storage is SQLite files that get processed and merged. The workflow looks like:

  1. Ingest raw data into SQLite (from CSV, API responses, etc.)
  2. Transform with SQL queries that produce new SQLite files
  3. Merge results into a final SQLite database
  4. Serve via Datasette for stakeholder access

No Spark cluster. No Airflow (though I use a simple cron + bash script for orchestration). No Postgres instance. The entire pipeline runs on a $20/month VPS and handles our daily analytical needs for a dataset of ~5M rows.

The limitation is tooling for non-technical users. Most BI tools (Tableau, Looker, Metabase) expect a database server they can connect to via JDBC/ODBC, not a file on disk. Metabase does support SQLite directly, which is great, but it’s the exception. If your stakeholders need self-serve dashboards with a traditional BI tool, you’ll still need to export to Postgres or use Datasette as the presentation layer.

Where I see this going:

The convergence of SQLite (OLTP), DuckDB (OLAP), and sqlite-vec (vector search) means you can build a surprisingly capable data platform with just embedded databases and no infrastructure to manage. For startups and small teams that don’t want to manage a data warehouse, this stack is remarkably powerful. The missing piece is a unified query layer that lets you join across SQLite and DuckDB seamlessly — but projects like PRQL and various DataFusion-based tools are working on that.

For developer-facing analytics and embedded dashboards, SQLite with Datasette is already remarkably capable. I’d encourage anyone doing lightweight analytics to try it before spinning up a Postgres instance or signing up for a cloud data warehouse.