SQLite — database is locked
Diagnose SQLITE_BUSY errors by shortening write transactions, enabling WAL and a busy timeout, and avoiding long-lived read locks.
Symptoms
- Error: SQLITE_BUSY: database is locked
- Writes fail intermittently while reads succeed
- The error appears when two processes or two requests write at the same time
- A transaction hangs until a timeout, then fails
Possible causes
- A write transaction is held open while waiting for network or user input
- Another connection holds a read lock in rollback-journal mode
- No busy_timeout is configured, so a lock conflict fails immediately
- A long-running query or a forgotten transaction keeps the database locked
- Multiple processes write to the same file on a network filesystem
Diagnosis steps
Reproduce and capture the exact failing statement
sqlite3 app.db 'PRAGMA journal_mode; PRAGMA busy_timeout;'Interpretation
A rollback-journal database with no busy timeout fails on the first conflict. WAL and a timeout are the baseline configuration.
Find long transactions and open statements
sqlite3 app.db 'PRAGMA lock_status;' # only during an active lock, and not available in all buildsInterpretation
If lock status is unavailable, instrument the application around BEGIN/COMMIT and statement finalization to find transactions that stay open.
Check for multiple writers and network filesystems
lsof app.db 2>/dev/null | headInterpretation
Several processes writing directly to one file increase contention. SQLite allows one writer at a time; a network volume can break locking semantics entirely.
Root cause
SQLite permits one writer at a time. A connection held a write lock longer than another connection was willing to wait, or rollback-journal mode blocked a writer behind a reader.
Fix
Enable WAL, set busy_timeout, keep write transactions short, finalize statements, and serialize writes through one process or a queue. Do not use a network filesystem for a database with concurrent writers.
Risk note
Retrying forever hides a transaction that never commits and can deadlock the application. Use a bounded retry with backoff and log the holder.
Verify the fix
Run concurrent writers for several minutes and confirm no SQLITE_BUSY errors appear. Check that WAL mode is active, busy_timeout is set, and every transaction commits or rolls back promptly. Add a test with a deliberately slow reader.
One writer at a time
SQLite serializes writes. When one connection holds a write transaction, another writer waits or fails. The default journal mode can also block a writer behind a reader. SQLITE_BUSY is the database telling you that the wait policy expired or no wait was configured.
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
WAL allows readers and a writer to proceed concurrently, and busy_timeout makes a writer wait instead of failing immediately. Set both on every connection; a pool that opens connections without these pragmas reintroduces the problem. WAL is persistent for the database file, but busy_timeout is per connection.
Keep transactions short
The most common cause is a transaction held open across an external call:
db.exec('BEGIN');
await fetch('https://slow.example.com'); // holds the write lock
db.exec('COMMIT');
Move network calls outside the transaction. Prepare data first, then begin, write, and commit. Finalize statements so they do not retain locks, and avoid long-running reads in the same process that writes.
Serialize writes when needed
If several processes must write, route writes through one process or a queue, or use a client/server database. A network filesystem can violate SQLite’s locking assumptions; do not put a concurrently written database on NFS or SMB. For read-heavy desktop or edge workloads, WAL plus a single writer is often enough.
Fix and verify
Enable WAL and a busy timeout, shorten transactions, and finalize statements. Verify with concurrent writers for several minutes and assert no SQLITE_BUSY errors. Add a test with a slow reader and a writer to confirm WAL behavior. A bounded retry with logging is acceptable for transient contention; an unbounded retry hides a transaction that never commits.