Start Here: The 30-Second Fix—Add a Busy Timeout
If you're getting SQLITE_BUSY, the first thing to check is whether you've set a busy timeout. SQLite, by default, doesn't wait—it fails instantly when it hits a lock. That's by design, but it's useless for most real-world apps.
In your connection code, set a timeout. In Python:
import sqlite3
conn = sqlite3.connect('your.db', timeout=5) # 5 seconds
In C or C++:
sqlite3_busy_timeout(db, 5000);In .NET (Microsoft.Data.Sqlite):
var conn = new SqliteConnection("Data Source=your.db;Default Timeout=5");That's it. If your app now waits and retries instead of dying, you're done. But know this: a timeout only masks the problem if you have a writer holding the lock for longer than your timeout. I had a client last month whose backup script ran a long VACUUM every night, and every other process would deadlock until we fixed the actual issue below.
The 5-Minute Fix: Switch to WAL Mode
If the timeout alone doesn't cut it, the next step is to enable Write-Ahead Logging (WAL). WAL changes how SQLite handles concurrency. In the default rollback journal mode, a writer blocks all readers and other writers. With WAL, readers don't block writers, and writers don't block readers. Only writers block each other—which is exactly what you need.
Run this once on your database:
PRAGMA journal_mode=WAL;You can also set it at connection time, but it's persistent after the first call. After switching, you'll see -wal and -shm files appear next to your database—that's normal, don't delete them.
I've seen this fix clear up 90% of SQLITE_BUSY errors in small web apps and desktop tools. One of my clients ran a Python Flask app with SQLite under Apache, and after enabling WAL, their error logs went silent.
If you're still getting locks after WAL, then you've got a real contention problem—someone is holding a write transaction open for too long.
The 15-Minute Fix: Find and Kill the Long Transaction
When WAL isn't enough, it's because you have a process that starts a write transaction, does a bunch of slow stuff, and then commits. During that time, every other writer gets SQLITE_BUSY. Your job is to find that transaction and shorten it.
Step 1: Check for open transactions
Run this query to see what's holding locks:
SELECT * FROM sqlite_master WHERE type = 'table';
-- Then check the database status:
PRAGMA database_list;
PRAGMA busy_timeout;
That won't show you the exact process, but it'll confirm the database is in WAL mode and the busy timeout is set.
Step 2: Look at your code for common culprits
- Long-running loops inside a transaction—If you do
BEGIN, then loop through 10,000 rows updating each one, you're holding the lock for minutes. Commit in batches of 500. - Nested transactions—If you call a function that opens a transaction while another is already open, you might be holding it longer than you think. Use
BEGIN IMMEDIATEif you're writing right away, and always commit or rollback in afinallyblock. - Interactive transactions—If your code waits for user input inside a transaction, that's a landmine. I once debugged a POS system where a cashier could leave the 'save sale' dialog open for an hour, locking the whole inventory table.
Step 3: If you can't find it, enable the busy handler with retries
Instead of just a timeout, use a busy handler that retries with exponential backoff. In Python, that looks like:
import sqlite3
import time
def retry_on_busy(db_path, max_retries=10):
for attempt in range(max_retries):
try:
conn = sqlite3.connect(db_path, timeout=1)
conn.execute('BEGIN IMMEDIATE')
# ... your write ...
conn.commit()
return
except sqlite3.OperationalError as e:
if 'database is locked' in str(e) and attempt < max_retries - 1:
time.sleep(min(2 ** attempt, 5))
else:
raise
But be honest: if you need that, it's a symptom that your architecture has a problem. The real fix is to make your transactions short and sweet.
When to Give Up on SQLite for Multi-Writer
If you've done all this and you still get SQLITE_BUSY under normal load, it's time to ask if SQLite is the right tool. SQLite is fantastic for single-writer scenarios—most web apps, desktop tools, embedded systems. But if you have multiple processes writing concurrently at high frequency, you'll eventually hit the wall. I've seen people try to band-aid it with BEGIN EXCLUSIVE and all sorts of hacks, but the truth is you need PostgreSQL or MySQL at that point.
I've done that migration twice for clients, and it's painful, but it stops the late-night pages.
Quick Troubleshooting Flow
- Set a busy timeout (30 sec)—If it stops, done.
- Enable WAL (5 min)—If it stops, done.
- Find and fix long transactions (15+ min)—If it stops, good.
- If still failing, seriously consider migrating to a server database.
One more thing: make sure you're not opening a new connection per query. That's a different beast—SQLite hates connection churn, and you'll see SQLITE_MISUSE more than SQLITE_BUSY, but it's still worth checking.