Debugging async code: the loop and the races between awaits
Async bugs look like load problems: everything is fine for one user and strange for many. The skill this section trains is asking, for every await and every blocking call, who else gets to run at that point.
One event loop, many requests
An async def handler runs on the event loop thread, which serves every request of the process. Any call that does not yield (CPU-bound work, time.sleep, a blocking library) stops the whole loop until it returns, so unrelated requests, even the health check, wait. Plain def endpoints and run_in_threadpool move work to worker threads. Reproduce it by timing a cheap endpoint while the suspect endpoint runs.
Await is a scheduling point
A single-threaded loop does not make code atomic. Between two awaits other requests run, and the database sees interleaved statements from many sessions. "Read the value, compute in Python, write it back" across awaits loses updates exactly like threads do. Push the arithmetic into one statement (UPDATE ... SET x = x + :n), or lock the row (SELECT ... FOR UPDATE); an in-process lock does not survive a second worker.