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 = NORMAL in WAL mode only syncs at checkpoints. It cannot corrupt the database, but the most recent commits can be lost after a power failure. Use FULL when that is unacceptable.
  • busy_timeout makes a blocked writer wait instead of failing at once with SQLITE_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 IMMEDIATE to 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-in bun:sqlite.
  • Edge and replication: libSQL (@libsql/client), the Turso fork.
  • Python: stdlib sqlite3, or aiosqlite in async code. Swift: GRDB.swift or Core Data.
  • prisma reaches SQLite through @prisma/adapter-better-sqlite3 or @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.