316 private links
The short version: one file, one process, and the heaviest page in the app served 315 million requests a day on a M1-laptop. For almost anything you are building, SQLite in WAL mode is enough, and the Postgres container you spun up out of habit was never needed.
The SQLite setup:
const db = new Database('chirp.db');
db.pragma('journal_mode = WAL'); // readers and the writer stop blocking each other
db.pragma('synchronous = NORMAL'); // fsync at checkpoints, not on every commit
db.pragma('busy_timeout = 5000'); // wait for the write lock instead of throwing
db.pragma('foreign_keys = ON'); // off by default, which surprises people
db.pragma('cache_size = -64000'); // 64MB of page cache
Under WAL, across every scenario, zero SQLITE_BUSY errors. Not "few." Zero.
Limits of SQLite:
- Reads stop scaling once anything writes.
- There is only one writer.
- One machine: the recovery time is always the time needed to restore a file.
Reach for Postgres when you have many writers contending on the same rows, when you need read replicas or automatic failover, when you need a real analytics engine over hundreds of millions of rows, or when your team genuinely needs the extension ecosystem. Those are real reasons."
Advantages of SQLite:
- Backups are a file copy. From the shell:
sqlite3 chirp.db "VACUUM INTO 'backup-$(date +%F).db'" - Local development is one file.
- Resetting the database is
rm - Tests get a real database each
- Deploys are a binary and a file
- There is nothing to operate
To optimize SQLite:
- focus on single-core speed
- buy enough RAM to hold the database: wait for GBs
- local NVMe. Never put SQLite on network storage.
- Pay for a dedicated core if you care about p99.
- Use
stricttables - Only INT, INTEGER, REAL, TEXT, BLOB, and ANY are allowed, and ANY is there when you actually want a key-value column. Store your JSON in a TEXT column and use the JSON functions on it.
- Prepare every query on startup. Making it on every request is a way to make SQLite look slow
The hardest part is user acquisition either way. Don't loose time on operations.
The reflex to start with a database server is a habit, not an engineering decision. It made sense when SQLite did lock the whole file on every write. That stopped being true a long time ago, and the tooling caught up: WAL for concurrency, STRICT for type safety, Litestream for replication.