The sqlite3.OperationalError: database is locked error happens when your code tries to write to an SQLite database while another connection already holds an open transaction on the same file. Fix it by enabling WAL journal mode, increasing the timeout, and ensuring connections are closed as soon as you're done with them.
Why it happens
SQLite's default journal mode is DELETE (sometimes called rollback journal). In this mode, the database allows only one writer at a time β and while a write transaction is open, other connections that attempt to write will immediately receive OperationalError: database is locked if they can't acquire the lock within the configured timeout.
The three most common triggers are:
- Two threads or processes each open their own
sqlite3.connect()and try to write concurrently. - A connection with an open transaction is never committed or closed before the next write attempt.
- A long-running read in a loop that (in some older SQLite versions) escalates to a shared lock and blocks a writer.
Reproducing the error
This minimal example reliably triggers the error:
import sqlite3
import threading
import time
DB = "myapp.db"
# Setup
with sqlite3.connect(DB) as c:
c.execute("CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, val TEXT)")
c.execute("INSERT INTO items VALUES (1, 'start')")
def slow_writer(name, hold=0.3):
conn = sqlite3.connect(DB, timeout=0.0) # fail immediately on lock
try:
conn.execute("BEGIN EXCLUSIVE")
conn.execute(f"UPDATE items SET val='{name}' WHERE id=1")
time.sleep(hold) # hold the lock open
conn.commit()
print(f"{name}: wrote OK")
except sqlite3.OperationalError as e:
print(f"{name}: ERROR β {e}")
finally:
conn.close()
t1 = threading.Thread(target=slow_writer, args=("writer_A", 0.3))
t2 = threading.Thread(target=slow_writer, args=("writer_B", 0.1))
t3 = threading.Thread(target=slow_writer, args=("writer_C", 0.1))
t1.start()
time.sleep(0.05) # let A grab the lock first
t2.start()
t3.start()
t1.join(); t2.join(); t3.join()
Output:
writer_B: ERROR β database is locked
writer_C: ERROR β database is locked
writer_A: wrote OK
writer_A held an exclusive lock for 0.3 seconds. B and C arrived while A was still open and, with timeout=0.0, failed immediately.
Fix 1 β Enable WAL journal mode (recommended)
WAL (Write-Ahead Logging) is a different journaling strategy that allows readers and writers to operate concurrently. Readers are never blocked by a writer, and a writer is never blocked by readers. Only writer-vs-writer contention still exists, but its window is much shorter because WAL commits are append-only and fast.
Enable it once when you first open the database:
import sqlite3
DB = "myapp.db"
def get_connection():
conn = sqlite3.connect(DB, timeout=30.0)
conn.execute("PRAGMA journal_mode=WAL")
return conn
# Verify it took effect
with get_connection() as conn:
mode = conn.execute("PRAGMA journal_mode").fetchone()[0]
print(f"Journal mode: {mode}") # Journal mode: wal
WAL mode persists on the database file β you only have to set it once, but calling PRAGMA journal_mode=WAL on every connection is harmless and makes the intent explicit. Two new files (myapp.db-wal and myapp.db-shm) appear alongside your database while connections are open. They are cleaned up automatically when the last connection closes.
Fix 2 β Increase the timeout
The default timeout is 5 seconds. For scripts that run serially and rarely overlap, simply giving SQLite more time to wait for the lock is often sufficient:
conn = sqlite3.connect("myapp.db", timeout=30.0) # wait up to 30 seconds
This does not eliminate contention β it just makes failures less likely under light concurrent load. For web applications or any code where multiple threads write to the same file, use WAL mode instead.
Fix 3 β Always close connections promptly
The most common source of long-held locks is code that opens a connection, writes, but forgets to commit or close it if an exception occurs. A with statement handles this automatically:
# Bad: exception skips conn.close()
conn = sqlite3.connect("myapp.db")
conn.execute("INSERT INTO items VALUES (2, 'data')")
conn.commit()
# if the code above raises, conn is never closed β lock stays open
# Good: context manager guarantees close even on exception
with sqlite3.connect("myapp.db") as conn:
conn.execute("INSERT INTO items VALUES (2, 'data')") # auto-commit on success
# conn is closed here
Important caveat: Python's sqlite3 context manager does not call conn.close() by default β it only commits or rolls back the transaction. The connection object is closed when it is garbage collected, but to be explicit and avoid surprises, call conn.close() after the with block when you are done with it.
Fix 4 β Share one connection per thread safely
If you use a thread pool and want all threads to share a single connection, you must disable SQLite's built-in thread check and add your own lock:
import sqlite3
import threading
conn = sqlite3.connect("myapp.db", check_same_thread=False)
conn.execute("PRAGMA journal_mode=WAL")
db_lock = threading.Lock()
def safe_write(value):
with db_lock:
conn.execute("INSERT INTO items VALUES (?, ?)", (None, value))
conn.commit()
Only use check_same_thread=False when you add your own serialization. Without the lock, two threads can corrupt the connection state even with WAL mode enabled.
Why WAL mode works: a short explanation
In DELETE mode, changes are written in-place and the original data is saved to a rollback journal. Any reader or writer must wait for the entire page to be safe. In WAL mode, changes are appended to a separate log file. Readers still see the last committed snapshot from the main database file and never have to wait for the writer. Writers append to the WAL file and only need a brief lock at commit time to record a checkpoint. The result is far less contention under typical mixed workloads.
Confirming the fix works
import sqlite3, threading
DB = "myapp.db"
errors = []
def writer(i):
try:
with sqlite3.connect(DB, timeout=5.0) as conn:
conn.execute("PRAGMA journal_mode=WAL")
conn.execute(f"UPDATE items SET val='writer_{i}' WHERE id=1")
except sqlite3.OperationalError as e:
errors.append(str(e))
def reader(i):
try:
with sqlite3.connect(DB, timeout=5.0) as conn:
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("SELECT val FROM items WHERE id=1").fetchone()
except sqlite3.OperationalError as e:
errors.append(str(e))
threads = [threading.Thread(target=writer, args=(i,)) for i in range(3)]
threads += [threading.Thread(target=reader, args=(i,)) for i in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(f"Errors: {len(errors)}") # Errors: 0
Output:
Errors: 0
When SQLite is not enough
WAL mode and careful connection management handle the vast majority of real-world concurrent-access scenarios for SQLite. If you need multiple processes writing to the same file at high throughput β especially from different machines β that is a sign to graduate to a client-server database like PostgreSQL. SQLite's own documentation is explicit: it is designed for local storage, not for high-concurrency server workloads.
Tested on Ubuntu 24.04 with Python 3.12.3 and SQLite 3.45.1.