Chapter 09 · Concurrency, Locking, WAL Mode, Busy Handling, and Checkpoints

Write-Ahead Logging: How WAL Changes Read/Write Concurrency

Build an accurate WAL mental model in which committed page versions are appended to a log, readers keep stable snapshots, writers overlap with readers, and one writer plus checkpointing remain fundamental constraints.

Beginner110–130 minutesConcurrent WAL snapshot labSQLite 3.53.4 baselineWAL requires same-host shared memoryLast reviewed: August 2026

Learning outcomes

Rollback mode protects the old database image in a journal while the database file is updated. Write-Ahead Logging (WAL) reverses that direction: the original database remains in place while changed page versions are appended to a separate WAL file. That inversion lets a reader keep an older snapshot while a writer commits newer versions. It improves read/write overlap, but it does not create multiple writers.

01

Explain WAL commit, reader end marks, and the wal-index from first principles.

02

Enable WAL and verify that the requested mode actually became active.

03

Identify the -wal and -shm companion files and treat them as database state.

04

Explain why WAL permits reader/writer overlap but still has one writer.

05

Demonstrate snapshot isolation with a long reader and a concurrent writer.

06

Explain persistence and same-host/local-filesystem requirements before choosing WAL.

Rollback journal versus WAL: reverse where the new pages go

QuestionRollback journalWAL
Where are old page versions protected?Copied to rollback journal before database pages are overwritten.Original database file remains the old base image until checkpoint.
Where do new page versions go during commit?Eventually into main database file.Appended to the -wal file.
Can readers overlap writer commit?Readers can overlap portions of write, but commit locking can conflict.Normally yes: readers use their snapshot/end mark while writer appends.
How many writers?One.Still one.
Extra maintenance stepJournal cleanup/recovery.Checkpoint WAL frames back into main database.

A WAL commit is an append plus a commit record

A WAL transaction writes changed page images as frames to the WAL. Commit is represented by a commit record in that WAL. The writer does not need to overwrite the corresponding main-database pages at commit time. Multiple committed transactions can accumulate in the same WAL until a checkpoint copies eligible frames back.

main database:  [page1 v1] [page2 v1] [page3 v1] ...
                         ^ stable base image

writer appends WAL:
                  [page2 v2][page7 v2][COMMIT A]
                                      [page2 v3][COMMIT B]

reader R started after COMMIT A:
  end mark --------------------^   (R ignores later COMMIT B)

checkpoint later copies safe committed page versions
from WAL back into the main database.

Readers get an end mark: one stable snapshot per read transaction

When a WAL reader begins, it records the location of the last valid commit in the WAL—its end mark. For a requested page, the reader uses the newest WAL version at or before that end mark, otherwise the main-database page. The end mark stays fixed for that read transaction, which is why a long reader can continue seeing an older value even after another connection commits a new one.

Scanning a large WAL for every page would be expensive, so SQLite maintains a wal-index in shared memory. The ordinary implementation uses a memory-mapped -shm file beside the database. That shared-memory design is the reason all WAL participants normally must be on the same host.

Enable WAL—and verify the return value

PRAGMA journal_mode=WAL; is a request that returns the resulting mode. Do not issue it and assume success. The VFS/environment must support WAL’s required shared-memory primitives.

sql · enable and verify WAL
PRAGMA journal_mode=WAL;-- expected on success: walPRAGMA journal_mode;-- expected: walPRAGMA wal_autocheckpoint;-- normally 1000, unless the build/default was changed

Unlike rollback modes such as TRUNCATE, WAL mode is persistent for the database. Close and reopen the file and PRAGMA journal_mode; should still report wal until the database is explicitly moved back to a rollback journal mode.

The -wal and -shm files are not junk

While WAL-mode connections are active, you can commonly observe fieldnotes.db-wal and fieldnotes.db-shm. The WAL file can contain transactions already committed from the application’s perspective but not yet copied into the main database file. SQLite’s documentation therefore treats the WAL as part of persistent database state: separating a live database file from its WAL can lose committed transactions or damage consistency.

Never “clean up” WAL companions by deleting them manually

Close database connections normally and let SQLite checkpoint/clean up. For backups and copies, use the reliable methods taught later in Chapter 16 rather than copying only the main file from a live WAL database.

Timeline comparison: the same long reader

ROLLBACK MODE
Reader A: BEGIN -- reads v1 ------------------------- COMMIT
Writer B:       BEGIN -- update v2 -- COMMIT?
                                      ^ may wait/fail on reader lock

WAL MODE
Reader A: BEGIN -- reads v1 ------------------------- COMMIT
Writer B:       BEGIN -- update v2 -- COMMIT succeeds
New reader C:                         BEGIN -> sees v2
Reader A:                              still sees v1

WAL improves reader/writer overlap. It does not add writer B2 simultaneously.

Concurrent snapshot lab

This lab proves the model using two connections. Disable autocheckpoint only for the short disposable experiment so the WAL remains visible. Re-enable normal policy or delete the disposable database afterward.

python · Python — one reader snapshot, one WAL writer
import sqlite3from pathlib import Pathp = Path("wal-snapshot-lab.db")for x in (p, Path(str(p)+"-wal"), Path(str(p)+"-shm")):    x.unlink(missing_ok=True)setup = sqlite3.connect(p, isolation_level=None)print(setup.execute("PRAGMA journal_mode=WAL").fetchone()[0])  # walsetup.execute("PRAGMA wal_autocheckpoint=0")setup.execute("CREATE TABLE reading(id INTEGER PRIMARY KEY, value TEXT NOT NULL)")setup.execute("INSERT INTO reading VALUES(1,'v1')")setup.execute("PRAGMA wal_checkpoint(TRUNCATE)")setup.close()reader = sqlite3.connect(p, isolation_level=None)writer = sqlite3.connect(p, isolation_level=None)reader.execute("BEGIN")print(reader.execute("SELECT value FROM reading WHERE id=1").fetchone()[0])  # v1writer.execute("BEGIN IMMEDIATE")writer.execute("UPDATE reading SET value='v2' WHERE id=1")writer.execute("COMMIT")  # succeeds while reader remains openprint(reader.execute("SELECT value FROM reading WHERE id=1").fetchone()[0])  # still v1fresh = sqlite3.connect(p)print(fresh.execute("SELECT value FROM reading WHERE id=1").fetchone()[0])   # v2fresh.close()reader.execute("COMMIT")reader.close(); writer.close()

The important state is not just “writer succeeded.” Reader A keeps its original snapshot, while a connection that begins afterward sees v2.

WAL still has one writer

All writers append to the one WAL stream, so SQLite still serializes write transactions. If writer A holds BEGIN IMMEDIATE, writer B can receive SQLITE_BUSY exactly as in Lesson 2. WAL is powerful for workloads with many readers and short writes; it is not a substitute for a client/server engine when many long writers must progress in parallel.

Same host is a correctness requirement

The default wal-index uses shared memory coordinated among processes on one machine. Official SQLite documentation states that WAL does not work over a network filesystem when participants are on different hosts. A network share that “looks local” in an application path does not remove this requirement.

DeploymentWAL judgment
Desktop app, one local machineCommon fit if driver/process coordination is correct.
Web service workers on one host sharing a local filePossible; test multi-process behavior and lifecycle carefully.
Containers on one host with a verified local volumePossible, but document volume/VFS/filesystem semantics.
Multiple hosts mounting one NFS/SMB databaseDo not treat as normal WAL deployment; use an architecture SQLite officially supports or move to a service database.

Persistence has cross-connection consequences

Once one connection successfully changes the file to WAL mode, other connections opening that same database operate with WAL mode too. That makes journal mode part of deployment state, not a private toggle for one request. Plan migrations, old runtimes, backups, and observability accordingly.

Lab verification

WAL checkpoint

Explain what changed and what did not.

  1. Where are new page versions written at WAL commit time?
  2. What is a reader end mark?
  3. Why can a reader continue seeing v1 after another connection commits v2?
  4. Does WAL permit two simultaneous write transactions on the same database file?
  5. Why must -wal not be manually discarded from a live database?
  6. Why is a cross-host network filesystem incompatible with normal WAL?
Review the answers

Committed page versions are appended to the WAL. A reader end mark identifies the committed WAL boundary for that read transaction, so its snapshot stays stable while later commits append beyond the mark. WAL still has one writer. The WAL can contain committed state not yet in the main file, so separating/deleting it is unsafe. Normal WAL requires a shared-memory wal-index visible to same-host processes, which cross-host network filesystems cannot provide.

Production judgment and bridge

Choose WAL because its snapshot/checkpoint model matches your workload, not because a blog says it fixes “database locked.” WAL shifts the dominant coordination problem from reader-versus-writer commit to writer serialization and checkpoint management. Lesson 4 makes checkpoint progress and WAL growth observable.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.