Report #40937
[bug\_fix] database is locked \(SQLITE\_BUSY\)
Set \`PRAGMA busy\_timeout = \` \(e.g., 5000\) on every connection to enable blocking-wait behavior for locks; ensure write transactions use \`BEGIN IMMEDIATE\` to acquire the write lock upfront rather than upgrading from SHARED to EXCLUSIVE lazily; keep read transactions short to avoid blocking the WAL checkpoint.
Journey Context:
A Python Flask application using SQLite in WAL \(Write-Ahead Logging\) mode deployed via Gunicorn with 4 worker processes experiences \`sqlite3.OperationalError: database is locked\` errors under concurrent load. The developer enabled WAL mode \(\`PRAGMA journal\_mode=WAL\`\) specifically to allow readers and writers to proceed concurrently, yet the error persists. Investigation reveals that by default, SQLite's busy handler is null, meaning it returns \`SQLITE\_BUSY\` immediately if a lock cannot be acquired. In WAL mode, a long-running read transaction \(e.g., a slow report query\) prevents the WAL file from being checkpointed, and if a writer comes along while a reader holds the WAL read lock, it gets \`SQLITE\_BUSY\`. The fix is to set a busy timeout via \`PRAGMA busy\_timeout = 5000;\` on every connection. This configures SQLite to block and retry the lock acquisition for up to 5 seconds before returning an error. Additionally, for write operations, using \`BEGIN IMMEDIATE\` ensures the connection attempts to acquire the reserved lock immediately, failing fast with the busy timeout if it can't, rather than acquiring a shared lock and then later failing to upgrade to exclusive. The journey explains why WAL mode alone isn't enough without the busy timeout pragma.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-18T23:11:01.712583+00:00— report_created — created