Async Python is not faster Python. It’s concurrent Python — which is a different thing entirely.
The event loop, explained simply
Python’s asyncio event loop runs on a single thread. When an await expression is hit, execution suspends and control returns to the loop, which can then run other coroutines.
This means: async is only useful for I/O-bound work. If your task is waiting for a network response, a database query, or a file read — async lets you do other things during the wait. If your task is computing (CPU-bound), async doesn’t help. You need processes.
The basic pattern
import asyncio
import httpx
async def fetch(url: str) -> str:
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.text
async def main():
urls = ["https://example.com", "https://httpbin.org"]
tasks = [asyncio.create_task(fetch(url)) for url in urls]
results = await asyncio.gather(*tasks)
return results
asyncio.run(main())
Both requests are in-flight simultaneously. With synchronous requests they’d run sequentially.
The mistake everyone makes first
Calling a blocking function inside an async function kills the event loop for every other coroutine:
async def bad():
time.sleep(2) # BLOCKS the entire event loop
data = open("file") # Also blocking
async def good():
await asyncio.sleep(2) # non-blocking
data = await asyncio.to_thread(open, "file") # runs in thread pool
asyncio.gather vs asyncio.TaskGroup
gather runs tasks concurrently and collects results. It’s the go-to for a fixed set of tasks.
TaskGroup (Python 3.11+) is structured concurrency — if any task raises an exception, the others are cancelled automatically. Prefer it for complex flows where you need cleanup.
When threads are better
- You’re calling a blocking library that has no async version
- You have CPU-light I/O mixed with some CPU work
- Your team isn’t familiar with async patterns — debugging concurrent async code requires mental overhead
concurrent.futures.ThreadPoolExecutor with asyncio.to_thread gives you threads from async code without rewriting your entire stack.
The rule I follow
If I’m writing a web server or an HTTP scraper, I use async (FastAPI / httpx). If I’m writing a data pipeline or a CLI, I use threads or processes. The performance difference rarely matters for one-off scripts; the complexity cost always does.