Blocking 호출 하나가 협력 스케줄링을 죽여
Python asyncio는 기본적으로 모든 걸 thread 하나 위에서 돌려. task 하나가 I/O를 기다리면 — DB를 읽든 HTTP를 쏘든 — event loop가 그동안 다른 task를 굴려. 이게 전부의 전제야.
함정은 여기 있어. asyncio는 await가 걸린 자리에서만 '기다리는 중'이라는 걸 알아채. 디스크에서 200ms 동안 붙들려 있는 sync 호출은 그 200ms 내내 event loop를 통째로 얼려. 다른 모든 request와 task와 websocket frame까지 같이.
그런데 평범한 sqlite3.connect(...)와 conn.execute(...)가 바로 그 sync C 호출이야. async 웹 앱 안에서는 DB query 하나하나가 event loop를 멈춰 세우는 셈이지. 유저가 한 명이면 티도 안 나. 동시에 100명이면 무너져.
해법은 둘이야.
- aiosqlite — SQLite를 백그라운드 thread에서 돌리고
execute나fetchone같은 걸 async로 열어줘. await하는 동안 DB 호출은 그 thread에서 돌고 event loop는 다른 일을 해. - asyncio.to_thread — sync 호출을 손으로 떼어 넘기는 방식이야. 되긴 하는데, aiosqlite가 이미 해둔 connection 관리를 다시 만들게 돼.
Self-reference: 피파 backend(
backend/store/conversations.py)가 aiosqlite를 쓰는 게 정확히 이 이유야. Claude가 SSE로 답을 흘리는 동안에도 conversation을 저장하면서 event loop를 계속 양보해야 하거든. sync로 썼다면 UI에서 눈에 보이게 끊겼을 거야.