sqlite3.OperationalError: database is locked is how Python reports SQLITE_BUSY: another connection holds a lock this one needs. Python already waits five seconds by default, so the usual causes are a transaction that was never committed, a cursor left half-read, or a read that turns into a write, which fails instantly whatever the timeout. The fix is to open connections in autocommit mode, wrap writes in an explicit BEGIN IMMEDIATE, use WAL mode, and give each thread and process its own connection.
What the error is
sqlite3.OperationalError: database is locked
The message is SQLite's text for result code 5. On Python 3.11 and later the exception also carries the precise code:
try:
conn.execute("UPDATE jobs SET state = 'done' WHERE id = 1")
except sqlite3.OperationalError as exc:
print(exc.sqlite_errorcode, exc.sqlite_errorname) # 5 SQLITE_BUSY, or 517 SQLITE_BUSY_SNAPSHOT
SQLITE_BUSY_SNAPSHOT means the connection read, someone else committed, and the connection then tried to write. A different message, database table is locked, is SQLITE_LOCKED: a conflict inside one connection, such as dropping a table while a cursor on it is still open. For the lock model behind both, see SQLITE_BUSY error: what it means and what causes it.
The timeout parameter
sqlite3.connect() takes timeout in seconds, default 5.0. It sets SQLite's busy timeout, which you can confirm:
import sqlite3
conn = sqlite3.connect("app.db", timeout=15.0)
print(conn.execute("PRAGMA busy_timeout").fetchone()) # (15000,)
So unlike most SQLite bindings, Python does wait by default. That shapes the diagnosis:
| The error arrives | Meaning |
|---|---|
| After the full timeout | Another connection held the write lock, or in rollback-journal mode a read lock, for longer than the timeout |
| Instantly | A lock upgrade was refused. The timeout does not apply to these |
Raising timeout helps only the first kind, and only if the other transaction is slow for a good reason. Most of the time it is slow because it was never finished.
How Python decides when a transaction starts
The module can manage transactions in four ways, and which one you are in determines where lock errors come from.
| Mode | How you get it | What it does |
|---|---|---|
| Legacy | The default: autocommit unset, isolation_level not None | Issues BEGIN implicitly before INSERT, UPDATE, DELETE and REPLACE only. SELECT and DDL run with no transaction. You call commit() |
| Legacy, manual | isolation_level=None | Never issues BEGIN. Every statement commits on its own unless you send BEGIN yourself |
| PEP 249 | autocommit=False (Python 3.12 and later) | A transaction is always open. commit() and rollback() immediately open the next one, using BEGIN DEFERRED |
| SQLite autocommit | autocommit=True (Python 3.12 and later) | Same behaviour as legacy manual; commit() and rollback() do nothing |
In legacy mode, isolation_level sets the kind of implicit BEGIN: "DEFERRED" (the default), "IMMEDIATE" or "EXCLUSIVE". It has no effect once autocommit is True or False. The Python documentation recommends the autocommit attribute over isolation_level and says the default will change to autocommit=False in a future release, so set the mode explicitly instead of relying on the default.
Why some failures are instant
SQLite will not wait when a connection that already holds a read snapshot asks for the write lock, because the current writer may be waiting for that reader to finish. In WAL mode it also refuses outright if anyone has committed since the snapshot was taken. Three Python patterns create exactly this situation.
A cursor that was not read to the end. In legacy mode a SELECT runs outside any transaction, but SQLite keeps a read snapshot for as long as the statement is unfinished:
import sqlite3
a = sqlite3.connect("app.db")
a.execute("PRAGMA journal_mode=WAL")
a.execute("CREATE TABLE IF NOT EXISTS jobs (id INTEGER PRIMARY KEY, state TEXT)")
a.executemany("INSERT INTO jobs (state) VALUES (?)", [("new",)] * 3)
a.commit()
b = sqlite3.connect("app.db")
cur = a.execute("SELECT id FROM jobs")
first = cur.fetchone() # rows remain: the snapshot stays open
b.execute("UPDATE jobs SET state = 'taken' WHERE id = 1")
b.commit() # another connection commits
a.execute("UPDATE jobs SET state = 'done' WHERE id = ?", first)
# sqlite3.OperationalError: database is locked (immediately, SQLITE_BUSY_SNAPSHOT)
Recovery needs two things: cur.close() (or reading the remaining rows) and a.rollback(). The implicit BEGIN succeeded before the UPDATE failed, so the connection is now inside a transaction that still holds the stale snapshot. A rollback alone, with the cursor still open, is not enough.
autocommit=False. Every statement, including SELECT, runs inside a deferred transaction. Any read followed by a write in the same transaction is an upgrade, and it fails with SQLITE_BUSY_SNAPSHOT if another connection committed in between. This mode is the standards-compliant one, but with several writers it produces more lock errors than the legacy default. There is no option to make it use BEGIN IMMEDIATE.
An explicit BEGIN followed by a read. BEGIN is BEGIN DEFERRED. If the first statement after it is a SELECT, the later write is an upgrade.
A complete SELECT followed by a write on a legacy-mode connection does not have this problem: the read finished, the snapshot was released, and the implicit BEGIN plus the write start from nothing, so the timeout applies.
The fix: explicit BEGIN IMMEDIATE
BEGIN IMMEDIATE takes the write lock at the start of the transaction, before the connection holds anything. The busy timeout applies to it, and once it succeeds in WAL mode nothing else in the transaction can fail with a lock error. Take transaction control away from the module and say what you mean:
import contextlib
import sqlite3
def connect(path):
conn = sqlite3.connect(path, timeout=5.0, isolation_level=None)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL")
conn.execute("PRAGMA foreign_keys=ON")
return conn
@contextlib.contextmanager
def write_transaction(conn):
conn.execute("BEGIN IMMEDIATE")
try:
yield conn
except BaseException:
conn.execute("ROLLBACK")
raise
else:
conn.execute("COMMIT")
conn = connect("app.db")
conn.execute("CREATE TABLE IF NOT EXISTS accounts (id INTEGER PRIMARY KEY, balance INTEGER)")
conn.execute("INSERT OR IGNORE INTO accounts VALUES (1, 100), (2, 0)")
with write_transaction(conn):
(balance,) = conn.execute("SELECT balance FROM accounts WHERE id = 1").fetchone()
if balance >= 10:
conn.execute("UPDATE accounts SET balance = balance - 10 WHERE id = 1")
conn.execute("UPDATE accounts SET balance = balance + 10 WHERE id = 2")
isolation_level=None works on every Python version. On 3.12 and later autocommit=True is the equivalent and is the clearer spelling. Reads outside write_transaction run in autocommit mode and never block writers in WAL mode. synchronous=NORMAL trades durability of the most recent commits on power loss for speed; leave it at the default FULL if that matters.
A shortcut exists for legacy mode: sqlite3.connect(path, isolation_level="IMMEDIATE") makes the implicit BEGIN an immediate one. It still only fires before INSERT, UPDATE, DELETE and REPLACE, so a read-then-write sequence reads outside the transaction, and an open cursor on the same connection still causes the instant failure. The explicit context manager has neither gap.
Forgotten commits and open cursors
When the error arrives after the full timeout, look for these.
- A write with no
commit(). In legacy mode the implicit transaction stays open, holding the write lock, untilcommit()orrollback(). Every other writer waits and then fails. If the connection is closed first, the changes are rolled back without any error. Checkconn.in_transactionat points where you expect no transaction. with conn:misunderstood. As a context manager, a connection commits or rolls back the open transaction on exit. It does not close the connection and it does not begin a transaction. Usecontextlib.closing()if you want it closed.- A long-lived connection in an interactive session. A notebook cell or REPL that ran an
INSERTand never committed blocks the application until the kernel exits. fetchone()orfetchmany()on a multi-row query. Callfetchall(), iterate to the end, orcursor.close(). In rollback-journal mode an unfinished cursor blocks every other connection's commit; in WAL mode it pins the WAL so checkpoints cannot finish.- Slow work inside the transaction. An HTTP request or a
time.sleep()betweenBEGIN IMMEDIATEandCOMMITholds the lock for its whole duration. Do it before the transaction starts. executescript(). It commits any pending transaction before running the script, which can end a transaction you meant to keep.
Threads
A connection may only be used in the thread that created it. Using it elsewhere raises ProgrammingError: SQLite objects created in a thread can only be used in that same thread. Passing check_same_thread=False turns the check off, and the documentation says you may then need to serialize writes yourself.
Sharing one connection between threads is rarely what you want even when it is safe, because a connection has one transaction. One thread's commit() commits another thread's half-finished work, and one thread's unread cursor pins the snapshot for all of them. Give each thread its own connection:
import sqlite3
import threading
_local = threading.local()
def get_conn(path="app.db"):
conn = getattr(_local, "conn", None)
if conn is None:
conn = sqlite3.connect(path, timeout=5.0, isolation_level=None)
conn.execute("PRAGMA synchronous=NORMAL")
_local.conn = conn
return conn
Threads then contend through SQLite's lock like separate processes do, and BEGIN IMMEDIATE with a timeout handles it. If write volume is high, a single writer thread that takes jobs from a queue.Queue and runs each in one transaction gives ordered waiting instead of polling; readers keep their per-thread connections.
sqlite3.threadsafety reports how the underlying library was built (3 means serialized, 1 means connections must not be shared). From Python 3.11 it reflects the real compile-time setting.
Multiprocessing and gunicorn workers
Each process needs its own connection, opened in that process. SQLite's documentation is blunt about inherited connections: do not open a connection, fork(), and use it in the child. Locks are tied to the process, so the result is lock errors at best and corruption at worst.
In practice:
- With
multiprocessing, callsqlite3.connect()inside the worker function or a pool initializer, never at module import time in the parent. - With gunicorn and
preload_app, application code is loaded in the master before workers are forked. A connection created at import time is inherited by every worker. Create connections lazily on first use inside the worker, or in apost_forkhook. - Processes can only coordinate through the file lock, so the settings carry the load: a timeout,
BEGIN IMMEDIATEfor writes, WAL mode, short transactions. Each added worker is another potential writer in the queue, and the timeout has to cover the queue.
The reproduction script in How to fix SQLITE_BUSY: a step-by-step playbook uses multiprocessing and shows each of those settings taking effect. It also has a retry wrapper for the BEGIN IMMEDIATE timeouts that remain under bursts.
SQLAlchemy
Two defaults matter. For a file database, SQLAlchemy 2.0 and later uses QueuePool and sets check_same_thread=False for you, so several pooled connections can be open at once. And the sqlite3 driver stays in legacy mode, so SQLAlchemy's "BEGIN" is not sent to the database until the first data-modifying statement.
The SQLAlchemy documentation gives an event recipe that turns off the driver's implicit BEGIN and emits one itself. The documented version emits a plain BEGIN; emit BEGIN IMMEDIATE to get the write lock up front:
from sqlalchemy import create_engine, event, text
engine = create_engine("sqlite:///app.db", connect_args={"timeout": 15})
@event.listens_for(engine, "connect")
def do_connect(dbapi_connection, connection_record):
# stop sqlite3 from emitting BEGIN on its own
dbapi_connection.isolation_level = None
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA journal_mode=WAL")
cursor.execute("PRAGMA synchronous=NORMAL")
cursor.close()
@event.listens_for(engine, "begin")
def do_begin(conn):
conn.exec_driver_sql("BEGIN IMMEDIATE")
with engine.begin() as conn:
conn.execute(text("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"))
conn.execute(text("INSERT INTO notes (body) VALUES (:body)"), {"body": "hello"})
With this hook every transaction on the engine takes the write lock, read-only ones included, so readers queue behind writers. If that hurts, create a second engine without the begin hook for read-only work.
Avoid connect_args={"autocommit": False} when several connections write: it is the PEP 249 mode described above, and read-then-write units of work fail with SQLITE_BUSY_SNAPSHOT. Pool size also matters less than it looks. More pooled connections do not add write capacity, since SQLite has one writer; they only lengthen the queue. A session held open across slow work holds its transaction too, so scope sessions tightly.
Django
Django 5.1 added two SQLite options. transaction_mode sets the kind of BEGIN that atomic() issues, and init_command runs pragmas on each new connection:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"OPTIONS": {
"timeout": 20,
"transaction_mode": "IMMEDIATE",
"init_command": "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;",
},
}
}
timeout is passed to sqlite3.connect(), in seconds. Django's documentation notes that with IMMEDIATE transactions should be as short as possible and discourages ATOMIC_REQUESTS in that case, because every request would hold the write lock for its entire duration. On versions before 5.1 the default mode is deferred and there is no setting for it.
aiosqlite
aiosqlite runs each connection on its own background thread and forwards extra keyword arguments to sqlite3.connect(), so timeout and isolation_level mean the same thing:
import asyncio
import aiosqlite
async def main():
async with aiosqlite.connect("app.db", timeout=5.0, isolation_level=None) as db:
await db.execute("CREATE TABLE IF NOT EXISTS hits (id INTEGER PRIMARY KEY, n INTEGER)")
await db.execute("BEGIN IMMEDIATE")
try:
await db.execute("INSERT INTO hits (n) VALUES (1)")
await db.execute("COMMIT")
except BaseException:
await db.execute("ROLLBACK")
raise
asyncio.run(main())
The asyncio-specific trap is sharing one connection between tasks. Two coroutines that interleave awaits on the same connection share its transaction, so one task's statements land inside the other's BEGIN. Use one connection per task for writes, or guard a shared one with an asyncio.Lock held for the whole transaction.
Common mistakes
- Raising
timeoutto fix an instant failure. Instant failures are lock upgrades. UseBEGIN IMMEDIATE. - Catching the error and carrying on. The transaction is still open. Roll back, and close any open cursor.
- Switching to
autocommit=Falsefor correctness without knowing it makes every read-then-write an upgrade. - Opening the connection at import time in a program that forks.
- Sharing a connection across threads or tasks to "avoid locking". It replaces a visible error with interleaved transactions.
- Enabling WAL and stopping there. WAL removes reader/writer blocking. Two writers still collide.
- Putting the database on a network share so several machines can reach it. File locking there is unreliable and WAL does not work across hosts.
Checklist
- Log
exc.sqlite_errornameand whether the failure was instant or after the timeout. - Open connections with
isolation_level=None(orautocommit=True) and wrap every write inBEGIN IMMEDIATE...COMMIT. - Set
PRAGMA journal_mode=WALand check that it returnedwal. - Read every result set to the end or close the cursor; roll back on every error.
- Keep network calls and slow work outside write transactions.
- One connection per thread and per process, opened after any fork.
- In SQLAlchemy use the
connectandbeginevent hooks; in Django 5.1 or later settransaction_mode. - Keep
timeoutlonger than the longest write transaction times the number of writers that can queue.
References
- sqlite3: DB-API 2.0 interface for SQLite databases (Python documentation), including transaction control
- SQLAlchemy SQLite dialect: transaction modes, the BEGIN event recipe, pooling
- Django database notes: SQLite and the Django 5.1 release notes
- aiosqlite API reference
- Gunicorn settings:
preload_appandpost_fork - SQLite transactions and result codes
- How to corrupt an SQLite database file, section on carrying a connection across
fork()
Get the weekly commit
New database deep dives every week.
