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.
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:
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:
- take ready callbacks and tasks
- run them until they yield or finish
- ask the operating system which sockets, pipes, or timers are ready
- mark the corresponding tasks as runnable
- 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:
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:
The loop stays responsive because the blocking function runs outside the event loop thread.
Common Pitfalls
- Thinking
asynciocreates 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
awaitmeans "run in background"; it really means "pause here until the awaited object is ready."
Summary
- '
asynciois 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.

