Network servers, clients and bots handle thousands of connections efficiently with asyncio.
async def defines a coroutine function; calling it returns a coroutine object without running anything. Inside, await suspends the coroutine until the awaited operation completes, handing control back to the event loop, which runs other coroutines in the meantime. All of this happens on a single thread: concurrency comes from cooperatively switching at await points, not from parallelism.
asyncio.run(main()) starts the loop and runs your top-level coroutine. To run things concurrently you must turn coroutines into tasks:
asyncio.TaskGroup(3.11) is the modern, safe way: tasks created withtg.create_task(...)run concurrently, theasync withblock waits for all of them, and if one fails the others are cancelled and errors are raised as anExceptionGroup(handle withexcept*)asyncio.gather(*coros)returns results in order and is still commonasyncio.timeout(seconds)(3.11) bounds how long a block may take
Awaiting coroutines one after another (await a(); await b()) runs them sequentially — the most common reason async code is mysteriously slow.
The cardinal rule: never block the event loop. A call like time.sleep(), requests.get() or a heavy computation freezes every other task. Use async libraries (asyncio.sleep, httpx, aiohttp, asyncpg), and push unavoidable blocking calls to a thread with await asyncio.to_thread(func, *args).
asyncio suits I/O-bound workloads with many waits. It does nothing for CPU-bound work, and mixing sync and async code adds complexity, so choose it deliberately rather than by default.
Example
import asyncio
import time
async def fetch(name: str, delay: float) -> str:
await asyncio.sleep(delay) # stands in for network I/O
return f"{name} done after {delay:.1f}s"
async def main() -> None:
start = time.perf_counter()
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(fetch(f"job{i}", 0.2 * i)) for i in range(1, 4)]
print([t.result() for t in tasks])
print(f"elapsed {time.perf_counter() - start:.1f}s") # ~0.6s, not 1.2s
try:
async with asyncio.timeout(0.1):
await fetch("slow", 1)
except TimeoutError:
print("timed out")
# run a blocking function without freezing the loop
result = await asyncio.to_thread(sum, range(1_000_000))
print(result)
asyncio.run(main())Remember
awaityields control to the event loop; everything runs on one thread- Use TaskGroup or gather to run coroutines concurrently
- Never call blocking functions in a coroutine; use
asyncio.to_thread
Watch out for
- Calling
time.sleep()orrequestsinside async code, stalling every task. - Awaiting coroutines one by one and expecting concurrency.