Random crashes. Database corruption. βdatabase is lockedβ errors.
Thatβs how my app Sortify behaved when multiple threads hit SQLite at the same time.
This post is how I fixed it properly β and made the database production-ready.
π§ The Problem: SQLite + Threads = Trouble
SQLite is lightweight and fast β but it has a big footgun:
β A single database connection shared across threads is NOT safe
In my app Sortify, multiple components were running concurrently:
- Auto-sort watcher
- Manual file operations
- Scheduler tasks
- Background processing threads
All of them were touching the same SQLite connection.
Symptoms I Saw
- Random crashes
-
database is lockederrors - Inconsistent history data
- Risk of database corruption
- App instability during concurrent operations
This line was the silent killer π
sqlite3.connect(db_path, check_same_thread=False)
It disables safety, but does not make SQLite thread-safe.
π₯ Why This Happens
SQLite allows multiple connections, but each connection must stay in one thread.
Sharing:
- β cursors
- β connections
- β transactions
across threads causes race conditions.
β The Solution: Thread-Local Database Manager
I implemented a proper thread-safe architecture using:
threading.local()- Per-thread SQLite connections
- Automatic retry logic
- Centralized DB access layer
π§© Introducing DatabaseManager
A brand-new module:
core/database_manager.py
Key Design Idea
Each thread gets its own SQLite connection
self._local = threading.local()
Connections are:
- Created on demand
- Stored per thread
- Automatically reused inside that thread
π Enforced Safety
sqlite3.connect(
db_path,
timeout=10.0,
check_same_thread=True # β
SAFE
)
If a thread tries to use another threadβs connection β SQLite blocks it immediately.
Thatβs what we want.
βοΈ Features of DatabaseManager
β Thread-Local Connection Pooling
Each thread has its own isolated connection
β Automatic Retry on Locks
Handles SQLiteβs infamous:
OperationalError: database is locked
with retry + backoff logic.
β Transaction Support
execute_transaction(operations)
Ensures atomic writes even under load.
β Clean Shutdown
close_all_connections()
No leaked file handles. No corrupted DBs.
π Fixing Existing Code
β Before: Shared Connection
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self.cursor = self.conn.cursor()
β After: Thread-Safe Manager
from .database_manager import DatabaseManager
self.db_manager = DatabaseManager(self.db_path)
Every database call now goes through one safe gateway.
π§Ό Removing Direct Cursor Access
β UI Code Touching DB Directly
cursor = self.history_manager.conn.cursor()
cursor.execute("DELETE FROM history")
β Proper Encapsulation
self.history_manager.clear_operations()
self.history_manager.clear_history()
No more hidden race conditions.
π§ͺ Stress Testing the Fix
I didnβt trust this blindly β I stress tested it hard.
Test Setup
- 5 threads
- 50 DB operations each
- 250 total concurrent writes
Results
Total operations: 250
Successful: 250
Failed: 0
Database records: 250
π Zero failures. Zero locks. Zero corruption.
π§ Thread-Local Connections Verified
β Number of unique connections: 3
β Each thread has its own connection
Exactly as designed.
π Impact
Before β
- Random crashes
- Locked database errors
- Unsafe concurrent writes
- App unstable under load
After β
- Fully thread-safe database access
- Stable concurrent operations
- No corruption risk
- Production-ready SQLite usage
ποΈ Files Changed
| File | Description |
|---|---|
core/database_manager.py |
New thread-safe DB layer |
core/history.py |
Migrated all queries |
ui/main_window.py |
Removed direct DB access |
tests/test_database_threading.py |
Stress test suite |
π Lessons Learned
- SQLite is thread-friendly, not thread-safe
-
check_same_thread=Falseis a trap - One connection per thread is the correct model
- Centralizing DB access prevents future bugs
- Stress tests reveal bugs unit tests wonβt
π Source Code
π¦ GitHub Repository:
π https://github.com/Mrtracker-new/Sortify
π Final Thoughts
This wasnβt just a bug fix β it was a foundational stability upgrade.
If your Python app:
- Uses SQLite
- Has background threads
- Randomly crashes under load
π This pattern will save you.
Happy coding! π
Top comments (0)