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

Readers, Writers, Locks, and Why SQLite Has One Writer at a Time

Understand how SQLite coordinates multiple connections to one database file, why rollback mode permits many readers but only one writer, and how to diagnose a controlled lock conflict before changing settings.

Beginner105–125 minutesTwo-connection rollback-lock labSQLite 3.53.4 baselineLocal filesystem requiredLast reviewed: August 2026

Learning outcomes

Chapter 8 gave one connection a correct transaction model. Concurrency begins when another connection opens the same database file while the first one is still reading or writing. SQLite must preserve isolation and atomicity across those independent actors, and it does so through the pager, journal/WAL machinery, and operating-system file locks. This lesson starts in the default rollback-journal model because its lock transitions make the single-writer rule easiest to see.

01

Distinguish connection, thread, process, and host/filesystem concurrency.

02

Explain the high-level SHARED, RESERVED, PENDING, and EXCLUSIVE lock states used by rollback mode.

03

Explain why many readers can coexist but only one write transaction can own writer capability for one database file.

04

Reproduce a controlled two-connection lock conflict without touching valuable data.

05

Read a lock-conflict timeline before reaching for timeouts or WAL.

06

Use a diagnosis checklist that finds transaction scope and ownership before tuning.

One file, two actors: what exactly is concurrent?

Two Python connections in one process, two browser threads, two command-line shells, and two unrelated programs can all reach the same SQLite file. They are not the same operational situation, even though the pager ultimately has to coordinate compatible file access.

Concurrency layerWhat it meansQuestion to ask first
Connection concurrencyTwo SQLite database handles are open on the same file.Are their transactions overlapping, and which one is writing?
Thread concurrencyTwo threads may call SQLite APIs concurrently.Does the driver permit a connection to cross threads, and what SQLite threading mode was built/configured?
Process concurrencyDifferent OS processes open the same path.Do both see the same physical file and functioning OS locks?
Host/filesystem concurrencyDifferent machines reach a shared path.Does the filesystem provide the locking/shared-memory guarantees required by the chosen journal mode?

A common debugging mistake is to collapse all four into “SQLite is locked.” Name the layer first. A database can be perfectly healthy while an application violates its own driver threading rules, or while two valid processes simply contend for the one writer.

The pager is the concurrency coordinator

SQLite’s pager is the layer that treats the database as fixed-size pages and coordinates caching, transactions, journal recovery, and file locking. In rollback-journal mode, developers usually need only a high-level picture of four lock states. These are database-file lock states—not row locks.

Lock stateHigh-level meaningWhat it permits
SHAREDA reader is using a consistent database snapshot.Multiple SHARED readers may coexist.
RESERVEDOne connection has declared/obtained writer intent while readers may still exist.Only one RESERVED holder for the file; existing/new readers can still obtain SHARED locks.
PENDINGA writer is waiting to reach the final exclusive phase and prevents new readers from arriving.Existing readers may finish; new SHARED locks are blocked.
EXCLUSIVEThe writer has exclusive file access for the phase that requires it.No other concurrent file access that conflicts with the exclusive operation.
Do not build application logic around lock-state trivia

The pager and VFS own these details. Learn the states to explain observed behavior, not to write code that polls OS locks or manipulates SQLite companion files manually.

Why “many readers, one writer” is a real engine rule

Many readers are compatible because they can all observe a stable committed database state. Two independent writers are harder: both could attempt to modify the same pages, journal state, schema, or freelist. SQLite serializes write transactions for a database file rather than exposing row-level write locks. In rollback mode, a writer can begin while readers still exist, but it must eventually reach a commit phase compatible with those readers finishing.

Connection A                         Connection B
------------                         ------------
BEGIN; SELECT ...
SHARED lock  ----------------------> reader active

                                    BEGIN IMMEDIATE;
                                    RESERVED / writer intent
                                    UPDATE ... (uncommitted)

A still reading  ------------------> writer may not finish exclusive commit yet
END A read                           |
                                    v
                                    commit can complete

Rule: readers may overlap portions of a write transaction,
      but there is still only one writer for the database file.

Rollback mode is not “the whole database is always exclusively locked”

The phrase “SQLite locks the whole database” is too crude to be useful. The lock is file-oriented, but rollback-mode version 3 deliberately delays the most exclusive phase so readers can coexist with a writer for part of the write transaction. The important application-level rule is narrower: only one connection can own the write transaction for a database file at a time, and commit may need readers to clear.

Chapter 8 connection

BEGIN IMMEDIATE is useful because it attempts to acquire write capability at the beginning. A failure there tells you about writer contention before the application performs the rest of its transactional work.

Controlled two-shell experiment

Use a disposable directory and one database file. The two shell windows must open the same resolved path. Set zero wait time so the conflict is visible immediately.

sql · one-time setup in shell A
sqlite3 concurrency-lab.dbPRAGMA journal_mode=DELETE;CREATE TABLE counter(id INTEGER PRIMARY KEY, value INTEGER NOT NULL);INSERT INTO counter(id,value) VALUES(1,0);.timeout 0

Leave shell A open and start an explicit writer:

sql · shell A — hold the write transaction
BEGIN IMMEDIATE;UPDATE counter SET value=value+1 WHERE id=1;SELECT value FROM counter WHERE id=1;-- A sees 1 inside its uncommitted transaction.-- Do not COMMIT yet.

Open shell B on the same file:

sql · shell B — second writer collides
sqlite3 concurrency-lab.db.timeout 0SELECT value FROM counter WHERE id=1;-- expected in rollback mode while A is uncommitted: 0BEGIN IMMEDIATE;-- expected: database is locked / SQLITE_BUSY

Now execute COMMIT; in A and retry BEGIN IMMEDIATE; in B. B should now acquire the writer position. The database was not corrupt and no “unlock command” was missing; B encountered ordinary contention.

A second rollback-mode timeline: a reader can delay commit

To expose the reader/writer relationship, let A hold an explicit read transaction while B writes. B can obtain writer intent and modify its private transactional state, but commit may need to wait/fail while A keeps a conflicting read lock.

sql · shell A — long reader
BEGIN;SELECT value FROM counter WHERE id=1;-- Keep this transaction open.
sql · shell B — write and attempt commit
.timeout 0BEGIN IMMEDIATE;UPDATE counter SET value=value+1 WHERE id=1;COMMIT;-- With A still holding the read transaction, COMMIT can report SQLITE_BUSY.

End A with COMMIT; or ROLLBACK;, then let B retry its commit if the transaction is still active. This is why an interactive shell left sitting after BEGIN can be a real concurrency participant.

Network filesystems are not “just another path”

SQLite relies on VFS/operating-system locking behaving correctly. The official locking documentation warns that network filesystem locking has historically been unreliable on some NFS and Windows-network configurations. WAL has an even stricter same-host shared-memory requirement that Lesson 3 will cover. Do not diagnose a shared-network deployment as if it were two local processes until you have verified the filesystem semantics SQLite requires.

Diagnose before tuning

When an application reports “database is locked,” capture state before adding a giant timeout or changing journal mode.

QuestionEvidence to collectWhy it matters
Which physical database file?Absolute/resolved path from each process; .databases.A typo can create a second file; aliases/network mounts can change locking assumptions.
Which connection owns a transaction?BEGIN/COMMIT/ROLLBACK logs; driver transaction state if available.Long or forgotten transactions are the most actionable cause.
Is the blocker reading or writing?Operation timeline and journal mode.Rollback readers can delay writer commit; writers conflict with writers in all modes.
How long is the transaction open?Monotonic timestamps around BEGIN and COMMIT.User think-time, HTTP calls, sleeps, or large loops often belong outside.
What journal mode is active?PRAGMA journal_mode;.Rollback and WAL have different reader/writer interactions.
What error code is it really?Primary/extended result code from driver.SQLITE_BUSY and SQLITE_LOCKED have different meanings.
Where is the file stored?Local disk, container volume, NFS/SMB/etc.Filesystem guarantees are part of correctness, not just performance.

Reproducible lab and cleanup

Repeat the two-shell writer conflict three times: first with a zero timeout, then after making the first transaction shorter, and finally after moving any artificial sleep/user interaction outside the transaction. Record who held the transaction and for how long. Do not leave a shell sitting inside BEGIN after the lab.

sql · clean end state
-- In both shells, ensure no explicit transaction remains.ROLLBACK;  -- harmless only if a transaction is active; otherwise expect an error.quit-- Delete concurrency-lab.db only after both processes have closed it.

Locking checkpoint

Reason from the timeline rather than memorizing an error string.

  1. Can two separate connections read one rollback-mode SQLite database at the same time?
  2. Can two separate connections own write transactions on the same database file at the same time?
  3. Why can a writer exist while a rollback-mode reader is still active?
  4. What does BEGIN IMMEDIATE reveal earlier than a deferred write?
  5. Why is a network-mounted path a correctness question, not merely a speed question?
  6. Name three things to collect before changing a busy timeout.
Review the answers

Multiple SHARED readers can coexist. SQLite serializes writers for one database file. Rollback mode delays the final exclusive phase, so a writer can hold writer intent and private changes while readers finish. BEGIN IMMEDIATE attempts writer acquisition at the start. Network filesystems must implement locking correctly, and WAL additionally requires same-host shared memory. Useful evidence includes resolved file paths, transaction start/end times, journal mode, the actual SQLite result code, and which connection is reading or writing.

Production judgment and bridge

The one-writer rule is not a defect to “turn off.” It is part of SQLite’s concurrency architecture. Your job is to keep write transactions small, identify contention precisely, choose the appropriate journal mode, and decide whether the workload still fits a single-file embedded database. Lesson 2 turns the observed writer collision into an explicit busy-wait and retry policy.

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.