How does asyncio's event loop work, and what does `async`/`await` actually do?

8 minadvancedasyncioasync-awaitevent-loop

Quick Answer

The **event loop** is a single-threaded scheduler that runs one coroutine at a time, switching to another whenever the running one hits an `await` on something not yet ready (I/O, a timer, another coroutine) — it's cooperative multitasking, not preemptive. `async def` defines a coroutine function; calling it returns a coroutine object (nothing runs yet); `await` suspends the current coroutine until the awaited thing completes, yielding control back to the event loop to run other ready tasks in the meantime.

Detailed Answer

async def creates a coroutine function

async def fetch_data():
    print("start")
    await asyncio.sleep(1)   # suspend here; event loop runs other work meanwhile
    print("done")
    return 42

coro = fetch_data()   # nothing has run yet -- just a coroutine object

Calling fetch_data() does not execute the body — like a generator, it returns a coroutine object that must be driven (via await, asyncio.run, or scheduled as a task) for its code to actually execute.

The event loop: single-threaded cooperative scheduling

import asyncio

async def worker(name, delay):
    print(f"{name} starting")
    await asyncio.sleep(delay)
    print(f"{name} done")

async def main():
    await asyncio.gather(
        worker("A", 2),
        worker("B", 1),
    )

asyncio.run(main())
# A starting
# B starting
# B done      <- after ~1s
# A done      <- after ~2s (not 3s! -- they ran concurrently)

asyncio.gather schedules both worker coroutines as concurrent tasks. When worker("A", 2) hits await asyncio.sleep(2), it tells the event loop "wake me up in 2 seconds, and meanwhile run something else" — the loop then runs worker("B", 1) until it also suspends. This is why the total time is ~2s (the max), not ~3s (the sum): the two sleep calls overlap because a single thread is interleaving them, not running them in true parallel, but scheduling them so neither blocks the other while waiting.

What await actually does

await only works on awaitables (coroutines, Tasks, Futures). It:

  1. Suspends the current coroutine at that point, saving its state (much like a generator's suspended frame — coroutines are, in fact, implemented on the same underlying mechanism as generators).
  2. Registers a callback so the event loop knows to resume this coroutine once the awaited thing completes.
  3. Returns control to the event loop, which picks another ready task/ callback to run.
  4. When the awaited operation finishes, the event loop resumes the original coroutine exactly where it left off, and await evaluates to the awaited thing's result.

Cooperative, not preemptive

Because there's no operating-system-level time-slicing, a coroutine that never awaits anything (e.g., a tight CPU-bound loop with no suspension points) blocks the entire event loop — no other coroutine gets to run until it returns. This is the single most important asyncio rule: only await genuinely yields control; ordinary synchronous code inside an async def function runs to completion without interruption.

Interview-ready summary: The event loop is a single-threaded scheduler running one coroutine at a time; await is the only point where control can be voluntarily handed back to the loop, letting other coroutines make progress while the current one waits on I/O. This cooperative model gives massive I/O concurrency on one thread, but a coroutine that blocks without awaiting stalls every other task.

Related Resources