๐Ÿ“

SQLite WAL Mode โ€” Pros and Cons

How Write-Ahead Logging solves SQLite concurrency problems

In SQLite's default mode (DELETE journal), reads are blocked during writes. WAL (Write-Ahead Logging) mode overcomes this limitation.

How It Works

In the default DELETE mode, changes are written directly to the DB file while the original is backed up to a journal file. In WAL mode, it's reversed โ€” changes are first recorded in a separate -wal file, then later applied to the DB file (checkpoint).

Thanks to this structure, readers access the original DB file while writers go to the WAL file, so they don't block each other.

Perfect Match with Solid Queue22

Solid Queue stores background jobs in SQLite with frequent INSERT/UPDATE operations. This is exactly the case WAL mode solves. In DELETE mode, web request DB reads would be blocked during job processing, causing response delays.

Enabling WAL Mode

Method 1: Rails 8 (database.yml) โ€” Recommended

Set journal_mode: wal in config/database.yml pragmas. This is the Rails 8 default.

Method 2: Direct SQL

sqlite3 storage/production.sqlite3 "PRAGMA journal_mode=WAL;"

Rails console: ActiveRecord::Base.connection.execute("PRAGMA journal_mode=WAL")

Once set, it's permanently stored in the DB file. WAL mode persists across app restarts.

Disabling WAL Mode (Revert to DELETE)

sqlite3 storage/production.sqlite3 "PRAGMA journal_mode=DELETE;"

Rails: ActiveRecord::Base.connection.execute("PRAGMA journal_mode=DELETE")

Caution: When switching WAL โ†’ DELETE, no open connections should exist. Stop the app first. After switching, -wal and -shm files are automatically deleted.

Checking Current Mode

sqlite3 storage/production.sqlite3 "PRAGMA journal_mode;"

Rails: ActiveRecord::Base.connection.execute("PRAGMA journal_mode").first

DELETE Journal vs WAL โ€” How They Work

Aspect DELETE (Default) WAL Mode
Write Method Direct overwrite to DB file Append to -wal file
Read + Write Concurrent No (reads blocked too) Yes
Write + Write Concurrent No No (Single Writer)
BusyException Frequent Dramatically reduced
Files Managed DB + journal (2) DB + -wal + -shm (3)
NFS Compatible Yes No (corruption risk)

Assessment for This Project

Concern Applicability
Multi-machine access No issue with single Fly.io machine
WAL file bloat SQLite auto-checkpoints (default 1000 pages)
Backup caution sqlite3 .backup command is safe
Solid Queue write contention Exactly the case WAL solves

Conclusion: Pros overwhelmingly outweigh cons. Rails 8 SQLite guide also recommends WAL mode in production.

Rails 8 database.yml Default Config

production: primary: <<: *default database: storage/production.sqlite3 pragmas: journal_mode: wal synchronous: normal mmap_size: 134217728 # 128MB journal_size_limit: 67108864 # 64MB busy_timeout: 5000 # 5 seconds

Checkpoint Types

Type Behavior Blocking
PASSIVE Applies what it can without interfering None
FULL Applies all frames (default auto) Minimal
RESTART FULL + restarts WAL from beginning Medium
TRUNCATE RESTART + truncates WAL to zero High

Key Points

1

Enable WAL mode: PRAGMA journal_mode=WAL (Rails 8 default)

2

Concurrent reader/writer access enabled โ€” impossible in DELETE mode

3

Auto checkpoint applies changes to DB when WAL reaches 1000 pages (~4MB)

4

Adjust Writer lock wait with busy_timeout (Rails 8: 5000ms default)

5

Include .sqlite3 + -wal + -shm in backups (sqlite3 .backup recommended)

Pros

  • Concurrent reader/writer access โ€” DELETE mode blocks reads during writes
  • Improved write performance โ€” frequent small transactions (exactly Solid Queue's case)
  • Dramatically fewer BusyExceptions โ€” readers and writers don't block each other
  • Fewer fsync calls โ€” only needed at checkpoints, reducing I/O overhead
  • Rails 8 default โ€” applied immediately with no extra configuration

Cons

  • WAL file bloat โ€” delayed checkpoints cause -wal file to keep growing (increased disk usage)
  • NFS/network filesystem incompatible โ€” corruption risk with multi-machine access (single Fly.io machine is fine)
  • Reads may be very slightly slower โ€” must check WAL file too (barely noticeable in practice)
  • Backup caution โ€” cannot copy just .sqlite3 file, must include -wal and -shm files
  • Three files to manage โ€” DB + -wal + -shm, must always handle as a set

Use Cases

Rails 8 + Solid Queue โ€” concurrent job writes and web request reads Solid Cache/Cable โ€” frequent small transactions for cache and WebSocket Read-heavy apps where occasional writes must not block reads Fly.io single machine deploy โ€” WAL's NFS limitation is not an issue