Python asynchronous processing practice

Python asynchronous processing practice

Late night debugging and diving into the abyss of Python asynchronous processing.

2am. With only the light from the monitor illuminating the room, I am once again stepping into the swamp of `asyncio`.

When developing a web application and needing to handle a large number of requests, everyone comes up with the magic word "asynchronous." If you adopt FastAPI, it will appear that asynchronous processing is being performed behind the scenes, but if you do not understand its contents correctly, you will not be able to do anything if the performance does not improve or the behavior becomes strange.

This time, it is not just an introduction to "how to use `asyncio`". I decided to write a technical memo about the mechanics behind asynchronous processing, and the pitfalls that beginners almost always fall into, that I've learned over the course of a number of projects.


1. The trap of confusing async and parallel

The first thing to clarify is that ``asynchronous processing = running simultaneously in multiple threads or multiple processes.''

Python's `asyncio` operates within a single process. Behind the scenes, a mechanism called the "event loop" manages tasks that are in a standby state. If you proceed with implementation without properly understanding this structure, you will later run into irreversible performance bottlenecks.

  • Synchronous: A situation where there is only one cook, waiting patiently in front of the stove until the water boils.
  • Asynchronous: While the cook is boiling water, the vegetables are being cut elsewhere. When you receive a notification (event) that the water has boiled, go back to the process.
This "effective use of waiting time" is the core of `asyncio`, and is the reason why FastAPI and aiohttp are considered to be fast. However, there is a fatal mistake that many developers make here. That is "mixing blocking processing into an asynchronous function".

Why is this deadly?

Suppose you run `time.sleep(5)` or some synchronous HTTP library (such as `requests`) inside an `async def`. At that moment, the event loop stops completely. All other tasks also stop and the entire application freezes.

This is a situation where the process becomes ``synchronous processing disguised as asynchronous.'' Without understanding this difference, a system that should handle thousands of requests can be wiped out by a single heavy computation or synchronous I/O operation. "Don't stop the event loop". This is a golden rule in asynchronous programming.


2. Practical configuration of FastAPI × aiohttp and the path to “true asynchronous”

FastAPI is a very powerful option for modern web development. However, just by adopting a framework, the contents will not be automatically optimized.

Especially when accessing external APIs, it is recommended to use an asynchronous client such as `aiohttp` or `httpx`. Let's compare the characteristics of commonly used libraries.

Featuresaiohttphttpx
Main usageAsynchronous web client/serverModern HTTP client (compatible with both Sync/Async)
Synchronous API supportNone (asynchronous only)Yes (operation similar to `requests`)
HTTP/2 compatibleNoYes
EcosystemVery mature and stableRelatively new and prefers type-safe designs

Personally, I choose `aiohmtp` when building complex asynchronous processing. However, if you just want to access the API, `httpx`, which has been trending in recent years, is also a very good option.

Example implementation: Retrieving data from multiple external sources

The following is a typical pattern for asynchronously processing multiple data sources in parallel within a FastAPI endpoint. Rather than simply waiting in sequence, use `asyncio.gather` (or `TaskGroup` in Python 3.11 and later) to run them simultaneously.

import asyncio
import httpx
from fastapi import FastAPI
from typing import List

app = FastAPI()
# Function to get data from external API (using aiohttp or httpx)
async def fetch_data(client: httpx.AsyncClient, url: str) -> dict:
    try:
        response = await client.get(url, timeout=10.0)
        response.raise_for_status()
        return response.json()
    except Exception as e:
        # In actual implementation, perform logging etc. appropriately
        print(f"Error fetching {url}: {e}")
        return {"error": str(e)}

@app.get("/aggregate")
async def get_aggregated_data():
    urls = [
        "https://api.example.com/data1",
        "https://api.example.com/data2",
        "https://api.example.com/data3"
    ]
    
    # Client for processing requests in parallel.
    # Ideally, you should not create it every time, but manage it with a context manager or lifecycle.
    async with httpx.AsyncClient() as client:
        # Here's the point: Issue three requests at the same time by using gather
        tasks = [fetch_data(client, url) for url in urls]
        results = await asyncio.gather(*tasks)
    
    return {"results": results}
`

The key to this code is that instead of waiting for each `await client.get(...)` in a loop, it stores the requests as tasks in a list and throws them all at once using `gather`. This allows most of the network latency to be consumed in parallel, dramatically shortening the overall response time.


3. The true nature of the “swamp” that you will always encounter on-site

From here on, I'll talk about key points in actual combat that you would never notice just by reading the documentation.

① How to isolate blocking processing

There are real cases where you absolutely have to use synchronous libraries (for example, for specific DB drivers, heavy image processing, etc.). At that time, simply ``giving up and writing at the same time'' is the worst choice.

In this case, use `run_in_executor`. This allows heavy processing to be relegated to another thread or process so that it does not occupy the main event loop.

import asyncio
import time
from concurrent.futures import ThreadPoolExecutor

# Functions that call heavy calculations or synchronization libraries
def blocking_task():
    # If this process takes a few seconds, the event loop will stop if you just write
    time.sleep(5)
    return "Finished"

async def main():
    loop = asyncio.get_running_loop()
    # Execute in a separate thread using ThreadPoolExecutor. The main loop becomes free.
    result = await loop.run_in_executor(None, blocking_task)
    print(result)
`

② Session reuse (Connection Pooling)

What beginners tend to do is create `aiohttp.ClientSession()` or `httpx.AsyncClient()` for each request. This leads to fatal performance degradation. This is because establishing a connection (TCP handshake and TLS negotiation) takes a considerable amount of time. Properly managing a connection pool for reusing these connections (for example, initializing and maintaining clients using FastAPI's `Lifespan` event) is the first step towards a professional-level implementation.

③ Timeout and Retry design

Since asynchronous processing often operates in "an unstable network environment," simply `await` is not sufficient. Setting strict timeouts using `asyncio.wait_for` and incorporating retry logic using Exponential Backoff are essential steps in creating a robust production system.

4. Summary: Async is not “magic”

Python's asynchronous processing is a powerful weapon, but it is also a double-edged sword that can seriously impair system stability if used incorrectly. To summarize again, the following three points are important.

1. Don't stop the event loop: Don't write `time.sleep` or synchronous I/O directly inside `async def`.
2. Reuse connections: Manage client sessions as long-lived objects whenever possible.
3. Be aware of concurrency: If you have multiple independent I/O operations, use `gather` or `TaskGroup` to maximize the benefits of concurrent execution.

After all, asynchronous programming is all about ``designing resources so that they don't wait unnecessarily.'' If you understand this philosophy, you won't have to worry about changing tools, whether it's FastAPI or aiohttp.

Now, it's time to make another cup of coffee and start working on the next bug fix.


🤖 Developer blog/public content

The developer's personal blog is also available, so please take a look. muttyan.com blog

Amazon great free trial/benefits summary