“Database is locked” tells you nothing about the file — and repairing it is the wrong move.
“Database is locked” is a statement about a lock, not about a file. In the runs below the identical message arrived after 0.00 s and after 2.00 s; nothing in the text says which one you got, who holds the lock, or whether the file is intact. It is also not a corruption message: the same database that raised it passed a byte-level check unchanged.
What was measured, on this machine (SQLite 3.45.1)
WAL reader (timeout=0) select count(*) -> (500,) 0.00s WAL writer (timeout=0) insert -> database is locked 0.00s WAL writer (timeout=2) insert -> database is locked 2.00s DELETE + BEGIN EXCLUSIVE, reader (timeout=0) -> database is locked 0.00s open a 0444 file read-write and insert -> attempt to write a readonly database
Two things follow. First, in WAL mode a reader is never blocked by a writer — it returned all 500
rows while the write transaction was open — so “locked” on a read usually means the file, not
the rows: a read-only mount, a missing -shm, or a handle on a different filesystem.
Second, the wait is the only thing that changed between the first and second writer: with
timeout=2 the same statement waits the full two seconds and then gives up with the same
words. The message is a timeout report wearing the costume of a diagnosis.
Two different errors share one sentence
SQLite has SQLITE_BUSY (“database is locked”) and SQLITE_LOCKED
(“database table is locked”). They are different codes with different causes, and people fix the
wrong one. The second one appears without any second process at all:
BEGIN IMMEDIATE; insert; PRAGMA wal_checkpoint(passive) -> database table is locked
That is one connection, one file, no contention — the checkpoint cannot run inside the write transaction it is part of. No amount of retrying fixes it; moving the checkpoint out of the transaction does.
The fix is a retry budget, and it is measurable
Four processes, 40 inserts each, into one WAL database:
busy_timeout=0s -> ok=[40, 0, 0, 0] errors=[0, 40, 40, 40] rows=540 wall=0.16s busy_timeout=5s -> ok=[40, 40, 40, 40] errors=[0, 0, 0, 0] rows=660 wall=0.54s
Same code, same file, same contention: 120 of 160 writes lost to the message versus none, and the only change is how long a blocked writer is willing to wait. If your application treats “database is locked” as “the database is damaged”, it will throw away 75% of its writes and then send the file to a repair tool.
“Locked” is not “corrupt” — check which one you have
The 20 KB database that raised the errors above, inspected byte by byte right after:
page_size=4096 journal_mode=WAL header_pages=5 file_pages=5 tail=0 pages freelist=0 change_counter=2 valid_for=2 · WAL_MODE_CLEAN: normal for a cleanly closed database, all commits are in this file => OK
The three page counts agree, there is no residue past the declared end, the log is clean. A lock
lives in the process table, not in the file, so no file checker can see it — and a repair
tool run against a locked-but-healthy database will rewrite the header and, in WAL mode, checkpoint
and delete the -wal, which is the only copy of the newest commits.
The cases where “locked” really is a file/reader problem
A snapshot taken while the app was running — app.db (4 KB) plus its
app.db-wal (12,392 bytes), but no -shm — placed on read-only media:
open mode=ro on a read-only dir, no -shm -> unable to open database file open mode=ro&immutable=1, same two files -> no such table: t dbcheck on the pair: page_size=4096 header_pages=1 file_pages=1 -wal=12392 frames=3 commit_pages=2 !! MAIN_DB_STALE: the newest commit lives in the -wal (it sets db size to 2), but this .db still says 1 page — copying only the .db loses those commits => CRITICAL
The -shm is a shared-memory index that WAL needs in order to open the log; without
write access to the directory, SQLite cannot create it and answers “unable to open database file” —
a message nobody associates with a lock. The usual advice for read-only media,
immutable=1, skips the log entirely and hands you an empty database that reports
no such table: three hundred committed rows, present on disk, invisible to the reader.
That failure looks exactly like corruption and is not.
Order of operations
- Read the code, not the text:
SQLITE_BUSYmeans wait and retry with a realbusy_timeout(and keep transactions short);SQLITE_LOCKEDmeans your own connection is fighting itself. - If a reader is locked in WAL mode, stop looking at transactions — look at the directory: is it
writable, does the
-shmexist, is this a snapshot on read-only media? - Only then inspect the bytes. If a
-walis present, check.dband-waltogether before restoring or repairing anything.
What was not tested, stated plainly: no NFS or SMB mount is available on this machine, so the
network-filesystem variants of this error (mmap of the -shm failing across hosts, stale
attribute caching, lock daemons) were not reproduced here and are not claimed. Everything above is a
local run on one ext4 filesystem, and the commands are short enough to repeat on your own share.
Check one .db file (max 20 MB; add the -wal if you have it)
Read-only byte inspection: page counts, WAL commit, freelist residue. The file is deleted before the answer is written, and nothing about its content is logged.