Home DevOps & Cloud Security Software Engineering AI & Machine Learning Web Development Developer Tools Programming Languages Databases Architecture & Systems Design Emerging Tech About
Programming Languages

Python sqlite3 OperationalError: database is locked, Fixed

NanoTech Insight
NanoTech Insight
2026-10-06
How this article was made: drafted with AI assistance and grounded in the sources linked at the end of the article. Spotted an error? Let us know.
SQLite database logo

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:

  1. Two threads or processes each open their own sqlite3.connect() and try to write concurrently.
  2. A connection with an open transaction is never committed or closed before the next write attempt.
  3. 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.

References

python sqlite3 sqlite database concurrency threading wal
NanoTech Insight
Published by
NanoTech Insight
Independent technology site

This article was drafted with AI assistance and grounded in the references linked above. Technology changes quickly, so please verify details against official documentation before relying on them in production.

Related Articles

Git Detached HEAD: What It Means and How to Fix It
2026-10-02
Python subprocess.run vs Popen: Which One to Use
2026-09-29
Python Performance Optimization: 8 Proven Techniques for 2026
2026-09-26
TypeScript Discriminated Unions and Type Guards Guide
2026-09-25
← Back to Home