The blocking call kills cooperative scheduling
Python's asyncio runs everything on a single thread by default. When one task is waiting on I/O (a database read, an HTTP request), the event loop can run other tasks. That's the entire premise.
The catch: asyncio only sees waits when you await. A synchronous call that blocks for 200ms on disk freezes the entire event loop for that 200ms — every other request, every other task, every websocket frame.
Plain sqlite3.connect(...) + conn.execute(...) are synchronous C calls. Inside an async web app, every database query stalls the event loop. With one user it doesn't matter. With 100 concurrent users it falls over.
Two solutions:
- aiosqlite — runs SQLite in a background thread and exposes async
execute/fetchone/etc. Awaiting them yields to the event loop while the DB call runs on the thread. - asyncio.to_thread — manually offload sync calls. Works but you'll reinvent aiosqlite's connection-management story.
Self-reference: Pippa's backend (
backend/store/conversations.py) uses aiosqlite for exactly this reason. SSE streaming from Claude must keep yielding to the event loop while a conversation is being persisted; a sync write would visibly stutter the UI.