Python Asyncio Event Loop — Core Concepts

What the Event Loop Actually Does

The asyncio event loop is the core scheduler behind Python’s async/await model. It maintains a queue of ready-to-run coroutines, watches file descriptors for I/O readiness, and fires timer callbacks — all inside a single thread.

Every iteration of the loop follows a cycle:

  1. Run all ready callbacks — anything that was scheduled with call_soon() or resumed after an await.
  2. Poll for I/O — ask the operating system which sockets or files are ready to read/write.
  3. Fire timers — check if any call_later() or call_at() deadlines have passed.
  4. Repeat.

This is sometimes called the “run-to-completion” model: once a coroutine starts running, it keeps going until it hits an await, at which point it voluntarily yields control back to the loop.

Starting and Stopping the Loop

The simplest entry point is asyncio.run(), which creates a new loop, runs your top-level coroutine to completion, and tears everything down:

import asyncio

async def main():
    print("Hello from the event loop")

asyncio.run(main())

Under the hood, asyncio.run() calls loop.run_until_complete(main()). You rarely need to touch the loop object directly, but it’s there via asyncio.get_running_loop() if you need to schedule low-level callbacks.

How Coroutines Get Scheduled

When you await something, the coroutine suspends and registers itself to be woken up when the awaited thing is done. The event loop doesn’t poll the coroutine — the coroutine tells the loop “wake me when this future resolves.”

asyncio.create_task() is how you schedule a coroutine to run concurrently:

async def fetch(url):
    # pretend network I/O here
    await asyncio.sleep(1)
    return f"data from {url}"

async def main():
    task1 = asyncio.create_task(fetch("url-a"))
    task2 = asyncio.create_task(fetch("url-b"))
    result1 = await task1
    result2 = await task2

Both tasks run during the same sleep window because the loop switches between them while both are waiting.

The I/O Multiplexing Layer

Under the hood, the event loop relies on the OS’s I/O multiplexing primitives — epoll on Linux, kqueue on macOS, IOCP on Windows. These system calls let the loop watch thousands of sockets efficiently without spinning.

When you open a network connection via asyncio.open_connection(), the loop registers the socket’s file descriptor with the OS selector. When data arrives, the OS wakes the selector, which wakes the loop, which resumes the coroutine that was waiting on that read.

Common Misconception

“Async makes things run in parallel.” Not exactly. The event loop runs one coroutine at a time. The performance gain comes from not wasting time waiting. If you have 100 network requests that each take 200ms of waiting, the loop fires them all off and waits for all of them simultaneously — wall-clock time is ~200ms, not 20 seconds.

True CPU-level parallelism still requires threads or processes. The event loop is about concurrency (managing many tasks), not parallelism (running tasks at the same physical time).

When the Loop Gets Blocked

Any synchronous, CPU-intensive call blocks the entire loop. A common trap:

import time

async def bad():
    time.sleep(5)  # blocks the whole loop for 5 seconds!

Use await asyncio.sleep() for delays, loop.run_in_executor() for CPU-heavy work, and keep your coroutines non-blocking.

One thing to remember: The event loop is a single-threaded scheduler that achieves concurrency by switching between tasks at await points — it’s fast because it never wastes time waiting, not because it runs things in parallel.

pythonconcurrencyasyncio

See Also

  • Python Actor Model Why treating each piece of your program like a person with their own mailbox makes concurrency way less scary.
  • Python Aiocache Caching aiocache remembers expensive answers so your async Python app doesn't waste time asking the same question twice.
  • Python Aiofiles Async Io aiofiles lets your async Python program read and write files without freezing — because normal file operations secretly block everything.
  • Python Aiohttp Understand Aiohttp through an everyday analogy so Python behavior feels intuitive, not random.
  • Python Anyio Portability AnyIO lets your async Python code work with any async library — write once, run on asyncio or Trio without changes.