Diagnosing and Fixing 'database is locked' (SQLITE_BUSY) Errors in SQLite
A diagnostic guide to SQLite's 'database is locked' (SQLITE_BUSY) error: how to tell journal-mode contention from stuck transactions, and which fix — WAL, busy_timeout, or code changes — matches each cause.
05 Aug 2025, 03:29 UTC

Your application intermittently throws database is locked (SQLITE_BUSY), usually under load or right after you added a second process or thread touching the same file. The fix is rarely one thing: SQLite is telling you that a connection tried to acquire a lock that another connection holds, and which lock — and who holds it — determines the remedy. This guide walks through recognizing the condition, identifying the cause, and applying the right fix.
What SQLITE_BUSY actually means
SQLite coordinates access to a database file with file locks. In the default rollback-journal mode (DELETE), a writer must obtain an exclusive lock on the whole database file, which blocks all other readers and writers until the transaction commits or rolls back. If your connection requests a lock it cannot get immediately, SQLite returns SQLITE_BUSY rather than waiting forever — unless you have configured it to wait.
Three conditions produce almost every SQLITE_BUSY you will see in production:
| Symptom | Likely cause | First check |
|---|---|---|
| Errors under concurrent load, single process | Default journal mode serializes writers; a slow write blocks others | PRAGMA journal_mode; |
| Error appears instantly, never retries | busy_timeout is 0 (the default in many drivers) | PRAGMA busy_timeout; |
| One writer stuck even with WAL enabled | A long-lived read transaction or an uncommitted write holds a lock | Audit transactions; check for leaked connections |
| Locked errors on a network share | NFS/SMB locking is unreliable; WAL requires working shared memory | Move the database to local disk |
Ordered diagnostic checks
Run these in order; each narrows the cause. All PRAGMA statements run inside any SQLite client (the sqlite3 CLI, or your application's connection) against the affected database file. They require no special OS permissions beyond read/write access to the file.
- Check the journal mode. Run
PRAGMA journal_mode;. If it returnsdelete,truncate, orpersist, you are in rollback-journal mode, where any write excludes all other access for its duration. If it returnswal, readers and one writer can coexist, so your problem is likely two writers or a stuck transaction. - Check the busy timeout. Run
PRAGMA busy_timeout;. A result of0means the connection gives up immediately when a lock is contested. Many language drivers leave this at zero unless you set it. - Find the lock holder. On Linux or macOS, run
lsof /path/to/database.dbfrom a shell (needs read access to the file; run withsudoif other users' processes may hold it). This shows which processes have the file open. A process you thought had exited — a stale worker, a leftover test, an open CLI session with an uncommitted transaction — is a classic culprit. - Audit your transactions. Look for
BEGINwithout a matchingCOMMIT/ROLLBACK, connections opened per-request but never closed, and long-runningSELECTstatements inside explicit transactions. In rollback mode, even a shared read lock blocks a writer's upgrade to exclusive.
Fixes tied to each finding
Finding: rollback-journal mode with concurrent readers and writers
Switch to WAL (Write-Ahead Logging) mode. In WAL, writers append changes to a separate -wal file and readers see a consistent snapshot, so reads no longer block the writer and vice versa. Enable it once — the setting persists in the database file:
PRAGMA journal_mode = WAL;Run this from any connection with write access. Verify with PRAGMA journal_mode; — it should return wal. Caveats: WAL creates -wal and -shm sidecar files, so naive file-copy backups can capture an inconsistent database; use the SQLite backup API or .backup in the CLI instead. WAL also requires reliable shared-memory locking, so it does not work correctly on most network filesystems (NFS, SMB). If the database must live on a network share, keep rollback mode and serialize access yourself, or move the file to local disk.
Finding: busy_timeout is zero
Set a bounded retry window so transient lock contention resolves itself instead of erroring:
PRAGMA busy_timeout = 5000;This is per-connection and must be set by every connection (or via your driver's equivalent, e.g., timeout in Python's sqlite3.connect()). 5000 ms is a reasonable starting point. Do not set it to tens of seconds as a band-aid: a high timeout turns a real lock problem into threads that appear hung, and can starve a connection pool. If raising the timeout merely delays the error rather than eliminating it, the lock holder is stuck, not busy — go back to step 3.
Finding: a leaked or long-lived transaction
Fix the code, not the pragmas. Ensure every transaction has a guaranteed commit/rollback (use try/finally or context managers), keep write transactions short, and never hold a transaction open across network calls or user input. In WAL mode, a read transaction that stays open for hours also prevents WAL checkpointing, letting the -wal file grow unboundedly — another reason to scope reads tightly.
Verifying the fix
Reproduce the original contention deliberately: run two shells, execute BEGIN IMMEDIATE; in one, then attempt a write from the other. With busy_timeout = 5000, the second write should pause and then succeed after you COMMIT the first; with the timeout at 0 it fails instantly. That contrast confirms the retry behavior is active. Then confirm PRAGMA journal_mode; and PRAGMA busy_timeout; return the expected values from your application's own connection, not just the CLI — drivers can override both.
When to escalate
If you have WAL mode enabled, a sane busy timeout, short transactions, and you still see persistent locking: check whether the file is on a network filesystem (move it), whether multiple processes legitimately need concurrent writes (SQLite supports only one writer — consider a client-server database), or whether an antivirus or backup agent is holding the file (common on Windows; check open handles with Resource Monitor or Handle.exe). Persistent SQLITE_BUSY after all of the above is usually an architecture signal, not a tuning problem.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.