BlogPython Async/Await Explained: What's Actually Happening Under the Hood
Web Development

Python Async/Await Explained: What's Actually Happening Under the Hood

By Madhukar May 6, 2026 6 min read

Python's async/await syntax trips up a lot of developers because it looks similar to async patterns in JavaScript but works differently. Once you understand the event loop model, everything clicks.

The Core Problem: Waiting for I/O

Synchronous Python code blocks. When you call requests.get("https://api.example.com"), the entire thread stops and waits for the network response — which might take 200ms. During that time, the thread cannot do anything else.

With async I/O, instead of blocking the thread, you register the I/O operation with an event loop and yield control back. The event loop can run other coroutines while waiting for the network response to come back.

python
import asyncio
import httpx

async def fetch_user(user_id: int):
    async with httpx.AsyncClient() as client:
        response = await client.get(f"/api/users/{user_id}")
        return response.json()

async def main():
    # These run concurrently — total time ~ max of individual times
    users = await asyncio.gather(
        fetch_user(1),
        fetch_user(2),
        fetch_user(3),
    )

What the Event Loop Actually Does

The event loop is a loop that continuously:

1. Checks if any pending I/O operations have completed

2. Runs the coroutines that were waiting on completed operations

3. Schedules new I/O operations

4. Repeats

It runs in a single thread. This is the key difference from threading. There is no parallelism — coroutines take turns running, yielding at each await point.

When Async Helps and When It Does Not

Async I/O is valuable when your code spends significant time waiting for external resources (databases, APIs, file I/O). FastAPI with asyncpg on a database-heavy web service handles dramatically more concurrent requests than a synchronous equivalent.

Async I/O does not help with CPU-bound work. If you are doing image processing, numerical computation, or any task that keeps the CPU busy, async only adds complexity. Use multiprocessing or offload to a worker queue.

python
# CPU-bound: async does nothing useful here
async def compute_something_heavy(data):
    result = await asyncio.to_thread(heavy_cpu_function, data)
    return result

asyncio.to_thread runs blocking functions in a thread pool without blocking the event loop — the right pattern for mixing CPU work with an async application.

M

Madhukar

Founder & Lead Engineer, Devpads

Building lightweight, high-performance, and privacy-first developer utilities. Madhukar specializes in modern web architectures, code editor tooling, and developer workspace experiences. Read more about our mission on our dedicated About Page or get in touch via Contact Us.

Stack: React · Vite · Tailwind · FastAPI · PostgreSQL