Overview
Use SQLite for single-writer workloads whose data fits on one local disk: local-first and edge apps, mobile, CI fixtures, embedded read-only data, and CLI tools. The current release is 3.53.4 (2026-07-24). Choose postgres when more than one process writes concurrently or when you need managed replication and failover.
Run a patched SQLite, especially in WAL mode
The WAL-reset bug can corrupt a database through a rare race between concurrent writes or checkpoints on different connections. It affects SQLite 3.7.0 through 3.51.2 and is fixed in 3.51.3 (2026-03-13) and later, with backports in 3.50.7 and 3.44.6. Check the version your process actually loads: SELECT sqlite_version();. Language drivers bundle or link their own copy, so upgrade the driver or system library, not just the CLI.
Choose SQLite for single-writer workloads
SQLite allows one writer at a time across the whole database, with any number of concurrent readers.
- Local-first and edge apps (Cloudflare Durable Objects, Turso) and mobile (core-data sits on SQLite).
- CI fixtures: a fresh
:memory:database per test beats starting Postgres. - Embedded read-only data: ship a prebuilt file and read it without a server.
Set the WAL pragmas on every connection
PRAGMA journal_mode = WAL; -- persistent: stored in the file
PRAGMA synchronous = NORMAL; -- per connection
PRAGMA foreign_keys = ON; -- per connection, off by default
PRAGMA busy_timeout = 5000; -- per connection
PRAGMA cache_size = -64000; -- 64 MB (negative = KiB)
PRAGMA temp_store = MEMORY;- WAL lets readers run alongside the writer. The journal mode persists in the file; the other pragmas must be set on each new connection.
synchronous = NORMALin WAL mode only syncs at checkpoints. It cannot corrupt the database, but the most recent commits can be lost after a power failure. UseFULLwhen that is unacceptable.busy_timeoutmakes a blocked writer wait instead of failing at once withSQLITE_BUSY.- Declare tables
STRICT(3.37 and later) so columns enforce their declared types instead of SQLite’s loose affinity.
Treat the writer as a serial queue
- Funnel multiple writer threads through one connection or queue.
- Open write transactions with
BEGIN IMMEDIATEto take the write lock up front instead of failing on upgrade from a read lock. - For batched inserts, prepare the statement once and run it in one transaction; per-row auto-commit pays a sync for every row.
- Avoid always having a reader open. Checkpoints cannot finish while overlapping readers never leave a gap, and the WAL file then grows without bound.
Back up with .backup or VACUUM INTO
Copying the file with cp while it is open can produce a corrupt backup. Use an online method.
sqlite3 app.db ".backup '/backups/app-$(date +%F).db'"
sqlite3 app.db "VACUUM INTO '/backups/app-$(date +%F).db';"Both produce a consistent snapshot. VACUUM INTO also rebuilds the file and drops free pages. For continuous backup, use Litestream to stream WAL changes to object storage.
Keep the file on a local disk
WAL needs shared memory between all processes using the database, so every process must run on the same host. NFS, SMB, and FUSE-mounted object stores break SQLite’s locking and can corrupt the file. If machines need shared access, use postgres or a hosted SQLite service such as Turso or Cloudflare D1. For read-only fan-out, copy the file to each reader’s local disk.
Pick the driver for the runtime
- Node:
better-sqlite3(synchronous; long queries block the event loop). Bun: built-inbun:sqlite. - Edge and replication: libSQL (
@libsql/client), the Turso fork. - Python: stdlib
sqlite3, oraiosqlitein async code. Swift: GRDB.swift or Core Data. - prisma reaches SQLite through
@prisma/adapter-better-sqlite3or@prisma/adapter-libsql.
For schema changes, SQLite 3.53 can add and remove NOT NULL and CHECK constraints with ALTER TABLE. Changing a column type still means rebuilding the table; see migrations.