← Debugging Guides

阅读中文版本

SQLite — database is locked

Diagnose SQLITE_BUSY errors by shortening write transactions, enabling WAL and a busy timeout, and avoiding long-lived read locks.

intermediatedebuggingsqlitedatabase

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 builds

Interpretation

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 | head

Interpretation

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.