sqlite3.OperationalError: database is locked
It shows up the moment a second process, thread or background job writes to the same SQLite file. Often it appears even after you've set a timeout, and even with WAL mode enabled. Here's why, and the settings that actually fix it.
The one rule: a single writer
SQLite allows many readers but only one writer at a time, for the whole database file. SQLITE_BUSY ("database is locked") means your connection needed a lock that another connection holds, and it gave up.
Two defaults make this much worse than it needs to be.
Problem 1: the busy timeout defaults to zero
Out of the box, SQLite doesn't wait at all. If the lock is taken, it fails immediately. Set a timeout on every connection:
PRAGMA busy_timeout = 5000; -- wait up to 5 s for the lock
sqlite3.connect("app.db", timeout=5.0) # Python sets busy_timeout for you
Many language bindings don't set one. Check yours.
Problem 2: deferred transactions can't wait
This is why you still get errors with a timeout. BEGIN starts a DEFERRED transaction: it takes no lock until the first read (shared lock), then tries to upgrade to a write lock on the first write.
BEGIN; -- deferred
SELECT balance FROM accounts WHERE id=1; -- read snapshot taken
UPDATE accounts SET balance = ...; -- needs to upgrade to a write lock
If another connection wrote and committed in the meantime, this transaction's snapshot is out of date. Waiting can't help, because the data it read has changed, so SQLite returns SQLITE_BUSY immediately and ignores busy_timeout. (In WAL mode this is SQLITE_BUSY_SNAPSHOT.)
The fix: start every transaction that will write with BEGIN IMMEDIATE:
BEGIN IMMEDIATE; -- take the write lock now; waits (busy_timeout) if it's held
SELECT balance FROM accounts WHERE id = 1;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
COMMIT;
The wait now happens at BEGIN, where the busy handler applies, and the transaction never has to upgrade. Most frameworks let you configure this. For example, Django 5.1+ has "transaction_mode": "IMMEDIATE" in the SQLite OPTIONS, and recent Rails versions use immediate transactions for SQLite by default.
Enable WAL mode
In the default rollback-journal mode, a writer blocks readers and readers block the writer from committing. WAL mode lets readers and the writer work at the same time:
PRAGMA journal_mode = WAL; -- persistent: set once per database
PRAGMA synchronous = NORMAL; -- safe with WAL, much faster commits
It's still one writer at a time, but reads no longer cause lock errors for writes.
Keep write transactions short
Every millisecond a write transaction is open, every other writer waits.
- Don't do network calls, file I/O or heavy computation inside a write transaction.
- Batch many small writes into one transaction. 1,000 inserts in one transaction is dramatically faster than 1,000 auto-committed ones.
- Close cursors and finalize statements. An unfinished
SELECT(a cursor you didn't read to the end, or a statement that was never reset) keeps a read transaction open. In rollback-journal mode that blocks writers from committing, and in WAL mode it stops checkpoints from resetting the WAL file.
Serialize writers in the app
When many threads or workers write, it's often simplest to send all writes through one connection (or a mutex or queue) and use a pool of separate read-only connections. That removes lock contention between writers entirely and matches SQLite's model.
Things that cause locking you can't fix
- Network filesystems (NFS, SMB, some Docker volume drivers on macOS and Windows). File locking is often unreliable there, so you get spurious lock errors or, worse, corruption. Keep the database on a local disk.
- Very high write concurrency from many machines. SQLite is an embedded database. If writes must come from many hosts, you've outgrown it: move to a client/server database or a replicated SQLite service.
A good baseline for every connection
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
Add BEGIN IMMEDIATE for write transactions, and most "database is locked" errors go away.
Checklist
- Set
busy_timeouton every connection. The default is 0. - Start write transactions with
BEGIN IMMEDIATEto avoid lock-upgrade failures. - Enable WAL mode with
synchronous = NORMAL. - Keep write transactions short and batched, and close cursors.
- Use a single writer connection, and keep the file on a local disk.
Get the weekly commit
New database deep dives every week.
