A strong answer
An asyncio event loop runs on a single OS thread around a tight loop (_run_once): (1) Poll the OS kernel I/O multiplexer (epoll on Linux, kqueue on macOS, IOCP on Windows) for sockets whose kernel buffers are ready to read/write, plus a min-heap of scheduled timer handles. (2) Move every ready callback into the _ready collections.deque. (3) Pop and execute each handle in _ready one by one (task._step()). A Coroutine (async def) is just a suspended frame object; wrapping it in an asyncio.Task drives it by calling coro.send(None). When the coroutine hits await future, it yields that Future object all the way up to Task._step(), which attaches self._wakeup to the Future and returns control to the event loop immediately! Because there is only 1 OS thread, any synchronous blocking call (requests.get, time.sleep, or 500 ms of pure CPU math) freezes _run_once so zero other tasks can advance until that call returns!
At most one Task executes Python bytecode on the event-loop thread at any instant, and cooperative context switches occur only at explicit await, async for, or async with suspension points—never preemptively between normal instructions. Because Task._step() runs synchronously on the event-loop thread until the coroutine yields a pending Future via await, two lines of code without an intervening await execute atomically with respect to all other coroutines on that loop—and any blocking C/OS call inside Task._step() stalls the entire loop.
An asyncio.Task + coroutine frame occupies only ~2–4 KB of heap memory (vs 8 MB of virtual stack for an OS pthread), allowing a single Python process to hold 50,000 concurrent idle network connections in ~150 MB of RAM. In production systems (uvloop (libuv Cython event loop), Uvicorn, FastAPI, aiohttp, httpx, and asyncpg all multiplex socket file descriptors through this exact Selector -> Future -> Task._step() state machine.), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.
What the interviewer is listening for
- Traces how the
asyncioevent loop (Selector,Future,Task, and coroutine suspension) operates in memory step by step rather than giving a vague textbook definition. - States the core correctness invariant and explains why edge cases do not violate it.
- Quantifies time, space, and hardware costs (OS Thread (
threading.Thread),asyncio.Task+ Coroutine) with concrete numbers. - Identifies real-world failure modes (Calling
requests,urllib,time.sleep, or sync DB drivers insideasync def) and explains how production systems mitigate them.
Common mistakes
- Treating the
asyncioevent loop (Selector,Future,Task, and coroutine suspension) as a black box without explaining the underlying memory or state transitions. - Ignoring worst-case tail latency or memory overhead (Assuming single-threaded
asynciomeans race conditions are impossible). - Forgetting boundary conditions and error recovery (Calling
coro_fn()withoutawaitorcreate_task()).