Transactions, locking and SQLITE_BUSY

SQLite · Practical · 14 min

Understanding SQLite's single-writer lock is the key to using it reliably from more than one thread or process.

SQLite allows many readers but only one writer at a time for the whole database. Transactions start with:

  • BEGIN (= BEGIN DEFERRED): takes no lock until the first read or write. A read-then-write transaction must upgrade its lock, and if another connection is writing, that upgrade can fail immediately with SQLITE_BUSY ("database is locked") — waiting wouldn't help because of the risk of deadlock.
  • BEGIN IMMEDIATE: takes the write lock right away (waiting for it per the busy timeout). Use it for any transaction that will write; it avoids the upgrade failure.
  • BEGIN EXCLUSIVE: in rollback-journal mode also blocks readers; in WAL mode it behaves like IMMEDIATE.

Set a busy timeout so connections wait for locks instead of failing: Python's sqlite3.connect(path, timeout=5.0) (5 seconds is the default) or PRAGMA busy_timeout.

Python's transaction control:

  • Legacy default: the module implicitly issues BEGIN (deferred) before DML; isolation_level="IMMEDIATE" makes that BEGIN IMMEDIATE, and isolation_level=None means you manage BEGIN/COMMIT yourself.
  • Python 3.12+: autocommit=False gives PEP 249 behaviour — a transaction is always open and commit() starts the next one; autocommit=True leaves everything to explicit SQL.

Keep write transactions short: while one is open, every other writer waits.

Example

import sqlite3, os, tempfile

path = os.path.join(tempfile.mkdtemp(), "app.db")
setup = sqlite3.connect(path)
setup.execute("CREATE TABLE counter (id INTEGER PRIMARY KEY, n INTEGER)")
setup.execute("INSERT INTO counter VALUES (1, 0)")
setup.commit()

a = sqlite3.connect(path, isolation_level=None)          # manual transactions
b = sqlite3.connect(path, isolation_level=None, timeout=0.1)

a.execute("BEGIN IMMEDIATE")                              # a holds the write lock
a.execute("UPDATE counter SET n = n + 1 WHERE id = 1")
try:
    b.execute("BEGIN IMMEDIATE")                          # waits 0.1 s, then fails
except sqlite3.OperationalError as e:
    print(e)                                              # database is locked
print(b.execute("SELECT n FROM counter").fetchone())      # (0,) readers still work
a.execute("COMMIT")
b.execute("BEGIN IMMEDIATE"); b.execute("UPDATE counter SET n = n + 1"); b.execute("COMMIT")
print(b.execute("SELECT n FROM counter").fetchone())      # (2,)

Remember

  • One writer at a time; readers continue
  • Use BEGIN IMMEDIATE for transactions that will write
  • Set a busy timeout so writers wait instead of failing

Watch out for

  • Starting a read-then-write transaction with plain BEGIN and getting random 'database is locked' errors under load.

Check yourself

  1. Why prefer BEGIN IMMEDIATE for a transaction that reads and then writes?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: sqlite.org/lang_transaction.html

Transactions, locking and SQLITE_BUSY · SQLite · Topolearn