Cooperative scheduling: coroutines yield at await points and the loop resumes ready tasks
asyncio is single-threaded cooperative multitasking. The event loop owns a ready queue of callbacks and a selector (epoll, kqueue, or IOCP) that reports when file descriptors are readable or writable. A coroutine runs until it hits an await on a future that is not yet done; it yields control back to the loop with a callback registered on that future. The loop then runs other ready callbacks. When the underlying IO completes, the future is marked done and its callback resumes the coroutine. Concurrency comes from overlapping waits, not from parallel execution: while one coroutine waits on the network, others run. That is why a single blocking call such as requests.get or time.sleep freezes the entire loop.
The loop is created per thread via asyncio.run or get_event_loop; only one loop runs per thread by default.
Tasks are scheduled coroutines; asyncio.create_task starts one immediately on the loop.
Selectors from the selectors module provide the OS-level readiness notifications.
asyncio.to_thread and run_in_executor move blocking work to a thread or process pool so the loop stays responsive.
Trade-off: cooperative scheduling has low per-task overhead and no data races within the loop, but any blocking call stalls everything.
Common mistake: calling time.sleep, requests, or a blocking database driver inside an async function. It blocks the entire service.
Version note: asyncio.run was added in 3.7. Tasks have set_name and eager task factories were added in 3.12. The loop API is stable but some methods (get_event_loop in the main thread) changed behavior in 3.10 and 3.12.
0-2 years experience
2-5 years experience
5-8 years experience
8+ years experience