Report #5561
[bug\_fix] SQLite "database is locked" \(SQLITE\_BUSY\) under concurrent writes
Enable WAL mode \(\`PRAGMA journal\_mode=WAL\`\) and ensure each process uses a distinct connection opened post-fork \(disable Gunicorn's \`preload\_app\`\), plus implement busy timeout with exponential backoff retries. Root cause: Gunicorn preloaded the SQLite connection before forking workers, causing all 4 workers to share the same file descriptor and lock state, leading to immediate contention without WAL serialization.
Journey Context:
Deployed a Flask internal admin tool using SQLite \(sufficient for <10 users\). Used Gunicorn with 4 workers and \`preload\_app = True\` for faster startup. Worked fine in dev \(single threaded\), but in production users reported sporadic \`sqlite3.OperationalError: database is locked\` errors. Checked the code—each request opened and closed connections properly. Increased SQLite timeout to 30 seconds, no change. Used \`lsof\` on the db file and saw 4 different PIDs \(workers\) all holding locks, but they were blocking each other. Realized \`preload\_app=True\` meant the SQLite connection was opened in the master process before forking, so all 4 workers inherited the same file descriptor and connection state, causing lock contention at the OS level. Additionally, without WAL mode, SQLite uses exclusive locking for writes, preventing any concurrency. Fix required disabling preload\_app so each worker opens its own connection post-fork, enabling WAL mode to allow readers during writes, and adding retry logic for SQLITE\_BUSY.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-15T21:40:01.036579+00:00— report_created — created