Asyncio (Coroutines, Tasks, Futures, and Event Loops)

03 Jun 2026, Updated: 29 Jul 2026 5 min read
1
Asyncio is Python's built-in library for writing asynchronous programs using the async and await syntax.

It enables a single thread to handle many I/O-bound operations concurrently by switching to other tasks whenever one task is waiting for an operation, such as a database query or network request, to complete.

In this article, we will explore asyncio from first principles and learn how coroutines, tasks, futures, and the event loop work together to provide concurrency in Python.

Why Asyncio Exists?

Before asyncio, Python applications typically handled concurrent work using threads. Consider a service that retrieves user information from a database.
def get_user():
    data = database.fetch_user()
    return data
While waiting for the database response, the thread remains blocked. Although the CPU is free to perform other work, the thread continues to occupy memory and operating system resources.

Asyncio was introduced to solve this problem by allowing applications to perform other useful work while waiting for I/O operations, such as database queries or network requests, to complete.
Asyncio provides concurrency, not parallelism. Most asyncio applications execute on a single thread using cooperative task switching.

Concurrency means multiple tasks make progress during the same period of time. Parallelism means multiple tasks execute simultaneously.

What is a Coroutine?

The foundation of asyncio is the coroutine.

A coroutine is a special type of function that can pause and resume its execution, allowing other coroutines to run while it waits for an operation to complete.

Often described as "lightweight threads" or "functions you can pause," they allow you to write complex, non-blocking asynchronous code in a clean, sequential style.

A coroutine is created using the async def keyword.
async def fetch_user():
    return {
        "id": 1,
        "name": "John"
    }
Calling a coroutine function does not execute it immediately. Instead, Python creates a coroutine object.
result = fetch_user()
print(result)
Output:
<coroutine object fetch_user>
A coroutine is essentially a suspended piece of work waiting to be scheduled by the event loop.

What is Event Loop?

The Event Loop is the heart of asyncio.

Every asyncio program runs on an event loop, which is responsible for scheduling tasks, resuming paused coroutines, monitoring I/O operations, and managing execution order.

Whenever a coroutine reaches an await point, control returns to the event loop. The event loop then schedules another coroutine that is ready to execute.

This allows many operations to make progress without requiring thousands of threads.

What is await?

The await keyword is one of the most important concepts in asyncio.
import asyncio
async def process():
    print("Start")

    await asyncio.sleep(5)

    print("End")
When Python reaches await asyncio.sleep(5), the coroutine pauses and yields control back to the event loop.

The coroutine is suspended, and the event loop is free to execute other ready tasks until the awaited operation completes.

What Happens Inside FastAPI?

Consider the following endpoint.
@app.get("/users")
async def get_users():
    users = await database.fetch_users()
    return users
While the database query is in progress, the event loop can process requests from other clients instead of leaving the thread idle.

This is one of the primary reasons FastAPI handles high-concurrency workloads efficiently.

What are Tasks?

A Task schedules a coroutine to run independently under the control of the event loop.
import asyncio

async def work():
    return "done"

task = asyncio.create_task(work())
A Task is a wrapper around a coroutine that allows the event loop to manage and schedule execution.

Think of it as a scheduled coroutine. Without a task, a coroutine simply exists. With a task, the event loop can execute it.

Running Multiple Tasks Concurrently

Consider three API calls.
async def service_a():
    await asyncio.sleep(2)

async def service_b():
    await asyncio.sleep(2)

async def service_c():
    await asyncio.sleep(2)
Sequential execution:
await service_a()
await service_b()
await service_c()
Total execution time: ~6 seconds.

Concurrent execution:
await asyncio.gather(
    service_a(),
    service_b(),
    service_c()
)
Total execution time: ~2 seconds. Because all three operations are awaited concurrently.

Understanding Futures

A Future represents a value that may become available later.
future = asyncio.Future()
Initially:
Result Not Available
Later:
future.set_result("completed")
Now the result exists. Tasks internally use Futures to track completion. In practice, developers interact with Tasks far more often than raw Futures.

Asyncio Does Not Help CPU-Bound Workloads

Consider:
async def calculate():
    total = 0

    for i in range(100000000):
        total += i

    return total
No I/O exists.

The coroutine performs only CPU-intensive work and never reaches an await expression, so it never yields control back to the event loop.

As a result, asyncio provides little or no performance benefit for this workload.

Asyncio is extremely effective for I/O-bound workloads but provides minimal benefit for CPU-intensive operations.

For CPU-heavy workloads, consider using multiprocessing, process pools, or distributed workers.
One of the most common mistakes is calling blocking functions inside asynchronous code. Example:
import time

async def bad():
    time.sleep(5)
This blocks the event loop. Because time.sleep() blocks the current thread, the event loop cannot execute any other coroutines during this period.

A better approach:
import asyncio

async def good():
    await asyncio.sleep(5)
The second version allows other tasks to execute while waiting.

Conclusion

Asyncio is the foundation of modern asynchronous Python applications and plays a critical role in FastAPI.

Coroutines define units of asynchronous work, Tasks schedule those coroutines for execution, Futures represent results that become available later, and the Event Loop coordinates everything.

Together, these components enable Python applications to handle thousands of I/O-bound operations efficiently using a single thread.
Nagesh Chauhan

Nagesh Chauhan

Principal Software Engineer • Java • Python • Distributed Systems • AI/ML

Principal Software Engineer with 14+ years of experience designing and delivering large-scale distributed systems, cloud-native applications, and AI-powered platforms.

Passionate about solving complex engineering problems using strong data structures and algorithms, along with expertise in Java, Spring Boot, Python, System Design, Microservices, Cloud, Kafka, Elasticsearch, and Generative AI.

Share this Article

💬 Comments

Join the Discussion