






















The team is provisioning a Postgres for a side project. Setup, monitoring, backups, connection pool tuning: about a day of work. Somebody asks “do we need Postgres?” The honest answer for a low-traffic SaaS is no. SQLite would handle it, with simpler ops, lower cost, faster reads, and zero network round-trips.
SQLite has a reputation as “the embedded DB on your phone,” and people miss that the same engine is genuinely production-grade for many web workloads. With WAL mode, Litestream for backups, and a single-process server, SQLite hits 50,000+ write/sec on a single laptop and is dramatically simpler to operate than Postgres.
This post is when SQLite is the right call, the patterns that work, and the realistic limits.
Three conditions:
1. The application has one writer process. SQLite supports concurrent reads but serializes writes. If your app is one Node.js / Go / Python process (or one process with worker threads), this is fine. If you want to scale horizontally to many writer instances, SQLite is the wrong tool.
2. Database fits on local disk. SQLite is a file. Most modern hardware has terabytes of fast local SSD. 100 GB of data is no problem.
3. Read-heavy or moderate write rate. SQLite reads are the fastest of any database: no network round trip, no serialization. Writes are bound by fsync. With WAL and synchronous = NORMAL, you can do tens of thousands per second.
The kinds of apps that match: side projects, internal tools, single-region SaaS up to ~100k users, edge / IoT applications, CLI tools that need to persist state.
app-1, app-2, app-3 instances all writing to the same DB. Possible with file locking, but slow. Not the right tool.For these, use Postgres or another full-featured RDBMS.
Three things to enable:
1. WAL mode. Default journal mode is “rollback” which serializes everything. WAL allows concurrent readers and one writer.
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
synchronous = NORMAL is safe with WAL; it relaxes fsync after every write. Combined, this is often 10× faster than the default config.
2. Reasonable cache and timeout.
PRAGMA cache_size = -64000; -- 64 MB cache
PRAGMA busy_timeout = 5000; -- 5 seconds before SQLITE_BUSY
PRAGMA foreign_keys = ON;
Litestream replicates every WAL frame to S3 (or compatible storage) in near real time. Set up once:
# /etc/litestream.yml
dbs:
- path: /data/app.db
replicas:
- type: s3
bucket: app-backups
path: prod
region: us-east-1
Run as a sidecar / systemd service:
litestream replicate -config /etc/litestream.yml
Restoring a database in disaster recovery:
litestream restore -o /data/app.db s3://app-backups/prod
Litestream gives you point-in-time recovery and continuous backup with seconds of RPO. Same operational guarantees as a properly-configured Postgres, much simpler to set up.
The Node ecosystem has two SQLite drivers. better-sqlite3 is the right one: synchronous, fast, simple API.
import Database from 'better-sqlite3';
const db = new Database('app.db');
db.pragma('journal_mode = WAL');
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
const select = db.prepare('SELECT * FROM users WHERE id = ?');
insert.run('Alice', 'alice@example.com');
const user = select.get(42);
Yes, it’s synchronous, but SQLite is fast enough that this is fine, and it removes the async-callback noise. For Node-based apps, the sync model is actually a feature.
For prepared statements with multiple inserts, use a transaction:
const insertMany = db.transaction((users) => {
for (const u of users) insert.run(u.name, u.email);
});
insertMany(largeArrayOfUsers);
Inside a transaction, SQLite batches the writes and runs orders of magnitude faster.
SQLite’s reader-writer concurrency:
For a typical Node.js app on one process, this maps cleanly. For Bun or other multi-threaded runtimes, the writer is a single connection, and multiple worker threads enqueue writes through it.
For Python with multi-process workers (gunicorn), be careful: each worker has its own connection, and concurrent writes from different processes will retry on SQLITE_BUSY and serialize. Tune busy_timeout and consider running through a single writer goroutine / worker.
Real-world numbers from a 2023 m1 MacBook Pro:
For most web workloads (a few thousand RPS, mostly reads, mostly cached), SQLite is genuinely faster than the network-attached Postgres alternative.
Fly.io and other “deploy code close to users” platforms have made SQLite practical for multi-region deployments via LiteFS. One primary writer, many read replicas distributed globally. Writes go to the primary; reads are local.
For a SaaS that mostly serves reads from a global user base, this gives Postgres-like multi-region semantics with a much simpler operational model.
SQLite has limited DDL support compared to Postgres. Some operations require recreating the table:
DROP COLUMN: supported since SQLite 3.35 (2021).RENAME COLUMN: supported since 3.25.For complex schema changes, the pattern is:
BEGIN;
CREATE TABLE users_new (...); -- new schema
INSERT INTO users_new SELECT ... FROM users;
DROP TABLE users;
ALTER TABLE users_new RENAME TO users;
COMMIT;
Most ORMs (Drizzle, Prisma) handle this automatically. For raw SQL, write a migration helper.
Type system is loose. SQLite stores any value in any column (“type affinity” instead of strict types). A TEXT column can hold integers. Use a strict ORM or runtime validation if this matters.
No native UUID type. Store as BLOB (16 bytes) or TEXT. Integer primary keys are still the fast default.
No JSONB. SQLite has JSON1 extension with json_extract etc. Slower than Postgres JSONB but functional.
Single-file is single-point-of-disk-failure. Mitigate with Litestream replication.
Locking on writes during long reads. A long-running read transaction can stall write commits in some configurations. Keep transactions short.
A few real signals:
The migration from SQLite to Postgres is straightforward. Both are SQL, both have similar feature sets at the application layer. Plan for a few days of work; switch ORMs over if needed; verify queries.
Don’t migrate prematurely. Many teams migrate to Postgres at the first sign of growth and end up running a 50 GB Postgres database with 10k DAU. SQLite would have been fine for years.
SQLite in 2024 is genuinely production-ready for a wide class of web applications. WAL mode, sensible pragmas, Litestream backups, all set up in 30 minutes. The reads are dramatically faster than network DB calls. The operational surface is “a file on disk.”
The next time someone says “we need a database for this side project,” ask whether SQLite would fit. For most projects under ~100k users, the answer is yes, and the simpler ops are a real ongoing benefit.
The kind of architectural judgment that picks the right-sized database for a workload (SQLite when it fits, Postgres when it doesn’t) is the kind of senior backend skill Yojji’s teams bring to client work.
Yojji is an international custom software development company founded in 2016, with teams across Europe, the US, and the UK. They specialize in the JavaScript ecosystem (React, Node.js, TypeScript), cloud platforms (AWS, Azure, GCP), and full-cycle product engineering, including the database choices that decide whether a system stays simple or accumulates infrastructure for its own sake.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。