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)