Unlocking Concurrent Reads in SQLite: A Practical Guide to Write‑Ahead Logging
Learn how SQLite’s Write‑Ahead Logging (WAL) mode eliminates "database is locked" errors in multi‑threaded apps. Enable WAL, monitor checkpoints, and balance disk usage for optimal concurrent read/write performance.
22 Sept 2025, 07:48 UTC

Problem: "Database is locked" in Multi‑Threaded Apps
When a SQLite database is opened in its default rollback journal mode, only one writer may hold the database lock at a time. In a busy web server, background worker, or mobile app with multiple threads, this leads to frequent database is locked errors and degraded throughput.
Why Write‑Ahead Logging (WAL) Helps
WAL separates the transaction log from the main database file. Writers append changes to a <database>-wal file while readers continue to read a consistent snapshot of the original <database>.db file. This architecture allows:
- Multiple concurrent readers without blocking each other.
- One writer that does not block readers.
- Improved I/O patterns on modern SSDs.
The trade‑off is the extra -wal and -shm files and the need for periodic checkpoints to merge WAL contents back into the main file.
Enabling WAL in Your Application
Set the journal mode once per connection, immediately after opening the database. The following command must be run in the same process that will perform subsequent reads/writes:
sqlite3 <database>.db "PRAGMA journal_mode=WAL;"
Typical expectations:
- SQLite creates
<database>.db-waland<database>.db-shmin the same directory. - Running
PRAGMA journal_mode;afterwards returnswal(case‑insensitive).
All connections that share the same database file should use the same mode; mixing wal and rollback across connections can raise errors.
Concrete Example: A Multi‑Threaded Reader/Writer
Below is a minimal Python example that demonstrates concurrent reads and a single writer. The code is illustrative; adapt it to your language of choice.
import sqlite3, threading, time
def writer(db_path):
conn = sqlite3.connect(db_path, timeout=30)
conn.execute("PRAGMA journal_mode=WAL;")
cur = conn.cursor()
for i in range(5):
cur.execute("INSERT INTO counter(value) VALUES(?);", (i,))
conn.commit()
time.sleep(0.5)
def reader(db_path, id):
conn = sqlite3.connect(db_path, timeout=30)
conn.execute("PRAGMA journal_mode=WAL;")
cur = conn.cursor()
while True:
cur.execute("SELECT COUNT(*) FROM counter;")
print(f"Reader {id}:", cur.fetchone()[0])
time.sleep(0.2)
# Setup database
conn = sqlite3.connect("demo.db")
conn.execute("PRAGMA journal_mode=WAL;")
conn.execute("CREATE TABLE IF NOT EXISTS counter(value INTEGER);")
conn.commit()
conn.close()
# Start writer and readers
threading.Thread(target=writer, args=("demo.db",), daemon=True).start()
for i in range(3):
threading.Thread(target=reader, args=("demo.db", i), daemon=True).start()
# Let threads run for a while
time.sleep(5)
print("Done")
While the writer commits, readers continue to query the snapshot without blocking. If you remove the PRAGMA journal_mode=WAL; statements, the readers will block until the writer finishes each commit, and you may see database is locked errors in a high‑traffic scenario.
Managing Checkpoints and Disk Footprint
WAL mode requires the database to periodically merge the WAL file back into the main file. SQLite does this automatically when the WAL size reaches a threshold (default 1000 pages). You can adjust this and control latency:
- Auto‑checkpoint size:
PRAGMA wal_autocheckpoint = 500;sets the threshold to 500 pages. - Force a checkpoint manually:
PRAGMA wal_checkpoint;which will block writers until the merge completes. - Enable full‑sync checkpoints for stronger durability:
PRAGMA checkpoint_fullsync = 1;. This can increase latency during the checkpoint.
Check the size of <database>.db-wal to monitor growth. A consistently small WAL file indicates checkpoints are happening regularly. If the file grows large, consider lowering wal_autocheckpoint or triggering manual checkpoints during low‑traffic windows.
Limitations and When WAL Is Not Ideal
- Read‑only media (e.g., network‑mounted read‑only file systems) cannot support WAL because the
-walfile must be writable. - Environments with strict file‑size limits may find the additional
-waland-shmfiles problematic. - Applications that rely on SQLite’s
backupAPI may need to handle the WAL file explicitly.
Actionable Checklist
- Open your database and run
PRAGMA journal_mode=WAL;once per connection. - Verify the mode:
PRAGMA journal_mode;should returnwal. - Look for
<database>.db-waland<database>.db-shmin the same directory. - Set
wal_autocheckpointto a value that balances checkpoint latency and disk usage for your workload. - Periodically monitor the WAL file size and consider manual checkpoints during maintenance windows.
- Ensure all processes that open the database use the same journal mode.
By following these steps, you can reduce lock contention, eliminate database is locked errors, and improve overall throughput for applications that perform frequent concurrent reads and writes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.