asyncio
Python
asynchronous programming
event loop
concurrency

How does asyncio actually work?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

asyncio works by running many cooperative tasks on a single event loop instead of giving each task its own blocking thread. The core idea is simple: when a coroutine reaches an await point for something not ready yet, it yields control so the event loop can run other work.

Coroutines, Tasks, and the Event Loop

An async def function returns a coroutine object. That object does nothing until the event loop schedules it.

python
1import asyncio
2
3
4async def greet():
5    print("start")
6    await asyncio.sleep(1)
7    print("end")
8
9
10asyncio.run(greet())

When await asyncio.sleep(1) happens, the coroutine does not block the whole process. It tells the event loop, "resume me later," and the loop is free to run something else in the meantime.

If you schedule several tasks, the loop switches among them whenever they yield:

python
1import asyncio
2
3
4async def worker(name, delay):
5    print(f"{name} start")
6    await asyncio.sleep(delay)
7    print(f"{name} done")
8
9
10async def main():
11    await asyncio.gather(
12        worker("A", 1),
13        worker("B", 2),
14    )
15
16
17asyncio.run(main())

This is concurrency, not parallel CPU execution. One thread is coordinating many tasks that spend much of their time waiting.

What the Event Loop Actually Waits On

Under the hood, the event loop keeps track of ready tasks, timers, and I/O events. On Unix-like systems it typically relies on selector mechanisms such as epoll or kqueue. On Windows it uses platform-specific event notification mechanisms.

At a high level, each loop cycle looks like this:

  1. take ready callbacks and tasks
  2. run them until they yield or finish
  3. ask the operating system which sockets, pipes, or timers are ready
  4. mark the corresponding tasks as runnable
  5. repeat

That is why asyncio is especially good for network servers, clients, and other I/O-heavy programs.

Futures Connect Waiting with Resumption

When a coroutine awaits something, it is often awaiting a Future or a task-like object. A Future represents a result that is not available yet. Once that future is completed, the event loop schedules the waiting coroutine to continue.

You can see the pattern in a simplified custom future example:

python
1import asyncio
2
3
4async def main():
5    loop = asyncio.get_running_loop()
6    future = loop.create_future()
7
8    loop.call_later(1, future.set_result, "ready")
9    result = await future
10    print(result)
11
12
13asyncio.run(main())

The coroutine pauses at await future. One second later, the loop marks the future done, and the coroutine resumes.

Why Blocking Code Breaks the Model

asyncio only works well when code cooperates. If you call a blocking function such as time.sleep(5) inside a coroutine, the event loop stops progressing and every other task stalls.

That is why blocking CPU-heavy or legacy synchronous work should be moved to threads or processes:

python
1import asyncio
2import time
3
4
5def blocking_work():
6    time.sleep(2)
7    return 42
8
9
10async def main():
11    result = await asyncio.to_thread(blocking_work)
12    print(result)
13
14
15asyncio.run(main())

The loop stays responsive because the blocking function runs outside the event loop thread.

Common Pitfalls

  • Thinking asyncio creates automatic parallelism for CPU-heavy code.
  • Calling blocking functions inside coroutines and freezing the event loop.
  • Creating coroutine objects without awaiting them or scheduling them as tasks.
  • Assuming await means "run in background"; it really means "pause here until the awaited object is ready."

Summary

  • 'asyncio is built around cooperative multitasking on an event loop.'
  • Coroutines run until they hit await, then yield control.
  • The loop resumes them when timers, I/O, or futures become ready.
  • This model is great for I/O-bound concurrency but not for CPU-heavy parallelism.
  • Blocking code must be moved off the event loop to preserve responsiveness.

Course illustration
Course illustration

All Rights Reserved.