Skip to content

Async Python

Introduction

Modern web applications demand concurrency — the ability to handle thousands of tasks at the same time. Traditionally, Python's synchronous, blocking nature made this difficult. But with the advent of asyncio, Python now supports asynchronous programming, enabling it to handle I/O-bound tasks far more efficiently.

This guide dives deep into how asynchronous Python works, why it's necessary, and how to write scalable, non-blocking code using async and await.


The Problem with Synchronous Code

Python executes one line at a time. When it hits a long-running operation like a database query or an HTTP request, the whole program stops and waits. That’s fine for scripts or small apps, but inefficient in systems that require concurrency — like servers or data pipelines.

Example:

python
import requests

def fetch_data():
    response = requests.get("https://api.example.com")
    return response.json()

data = fetch_data()
print(data)

This will block until the network request completes. During that time, Python cannot do anything else — no other tasks, no serving other users.

Now imagine you're building a server that needs to serve 500 clients simultaneously. Blocking each call like this would kill performance.


What Async Solves

Async Python doesn't use threads to solve this — it uses something called the event loop, which lets Python switch between multiple "tasks" during I/O waits.

This model is useful when your code spends a lot of time waiting — for HTTP responses, file reads, database queries, etc.


Sync vs Async

Let’s illustrate the difference with a simple diagram.

Synchronous Execution

Task A: [==== wait 3s ====]
Task B:                  [==== wait 3s ====]
Total Time: 6s

Asynchronous Execution

Task A: [== initiated ==]     [== continues ==]
Task B:       [== starts ==]     [== finishes ==]
Total Time: ~3s

In async mode, while Task A is waiting (e.g. for a network response), Task B runs. We achieve concurrency, not through threads, but via non-blocking I/O.


Core Concepts of Async Python

1. Coroutines

A coroutine is a function defined with async def. It's not executed immediately — instead, it returns a coroutine object. You need to await it.

python
async def my_coroutine():
    print("Start")
    await asyncio.sleep(1)
    print("End")

2. The Event Loop

The event loop runs in the background, managing coroutines and deciding when to pause or resume them.

python
import asyncio

async def main():
    await my_coroutine()

asyncio.run(main())

3. Awaitables

An object is awaitable if it can be used with the await keyword. These include:

  • Coroutines
  • Tasks (created with asyncio.create_task)
  • Futures

Practical Example: Concurrent Tasks

Let’s simulate a blocking API with async.

python
import asyncio

async def get_data(x):
    print(f"Fetching {x}")
    await asyncio.sleep(2)
    return f"Result {x}"

async def main():
    task1 = asyncio.create_task(get_data(1))
    task2 = asyncio.create_task(get_data(2))
    result1 = await task1
    result2 = await task2
    print(result1, result2)

asyncio.run(main())

Output:

Fetching 1
Fetching 2
Result 1 Result 2

Even though both tasks wait 2 seconds, they run concurrently. So total time ≈ 2 seconds instead of 4.


More Async Tools in Python

asyncio.gather()

Runs multiple coroutines in parallel and returns their results.

python
results = await asyncio.gather(get_data(1), get_data(2), get_data(3))

asyncio.sleep()

An async version of time.sleep(). It simulates delay without blocking the event loop.


Mixing Async and Sync Code

You cannot use await in a regular (non-async) function.

python
def wrong():
    await asyncio.sleep(1)  # SyntaxError

Always wrap your async code in an async def and use await inside it.


Real-World Applications

  1. Web frameworks: FastAPI, Nexios, Sanic — all use async to handle thousands of HTTP requests concurrently.
  2. WebSocket servers: Real-time systems like chat, stock dashboards.
  3. Web scraping: Using aiohttp and asyncio.gather() to fetch many pages in parallel.
  4. Task pipelines: Running I/O-heavy tasks concurrently.

When Async is NOT Helpful

Async is not helpful when:

  • Your code is CPU-bound (e.g., image processing, machine learning). Use multiprocessing or native threads for that.
  • You have complex call stacks that become hard to reason about.
  • You're dealing with libraries that don't support asyncio.

Comparison: Threads vs Async

FeatureThreadsAsyncio (Async/Await)
Concurrency modelPre-emptiveCooperative (event loop)
Memory usageHigherLower
ComplexityMediumLow to Medium
Best forCPU-bound tasksI/O-bound tasks

External References


Final Thoughts

Async Python is powerful but comes with a different mindset. If you’re building high-performance servers (like Nexios), learning async is not just useful — it’s essential.

Focus on mastering:

  • async def and await
  • asyncio.run, asyncio.create_task, asyncio.gather
  • Writing non-blocking logic

Once you internalize this pattern, you'll build faster, more efficient systems — the kind of work that scales under load and feels snappy to users.

Let me know if you want the same broken down for Nexios’s core — like how to use async in routing or middleware.