asyncio — async/await and the Event Loop
Advancedasyncio runs thousands of concurrent I/O operations on one thread: coroutines pause at await, the event loop switches to whoever is ready, and gather fans out work — as long as nothing blocks the loop.
Overview
asyncio is cooperative multitasking: an async def function is a coroutine that runs until it hits await, then hands control back to the event loop, which resumes some other coroutine whose I/O completed. One thread, no locks for most code, and scaling to tens of thousands of concurrent connections — this is the engine under FastAPI, aiohttp, and every modern Python web stack. The model has one iron rule: never call blocking functions (time.sleep, requests.get, heavy CPU loops) inside a coroutine, because they freeze the single thread and every other task with it. The fix is an async library (httpx, asyncpg) or asyncio.to_thread for the stubborn cases.
Coroutines, await, and gather
Calling an async function returns a coroutine object; nothing runs until it is awaited. asyncio.gather starts several at once and their waits overlap — three service calls finish in the time of the slowest one, not the sum.
import asyncio
import time
async def call_service(name, delay):
await asyncio.sleep(delay) # yields to the event loop while "waiting"
return f"{name}: ok ({delay}s)"
async def main():
start = time.perf_counter()
results = await asyncio.gather(
call_service("orders", 1.0),
call_service("payments", 1.5),
call_service("inventory", 1.2),
)
for r in results:
print(r)
print(f"total: {time.perf_counter() - start:.1f}s") # ~1.5s, not 3.7s
asyncio.run(main()) # the ONE entry point — creates and closes the loop
# create_task: start now, await later
async def dashboard():
task = asyncio.create_task(call_service("analytics", 2.0))
print("doing other work while analytics runs...")
print(await task)The Iron Rule — Never Block the Loop
One blocking call stalls every coroutine in the process. In a FastAPI service, one time.sleep(5) inside an async endpoint freezes all in-flight requests for 5 seconds. Use async-native libraries, or push blocking work to a thread with asyncio.to_thread.
import asyncio
import time
async def bad():
time.sleep(2) # ✗ BLOCKS the whole event loop for 2s
# every other task in the process is frozen meanwhile
async def good():
await asyncio.sleep(2) # ✓ loop keeps serving other tasks
def legacy_report(): # blocking library you cannot change
time.sleep(2)
return "report ready"
async def main():
# to_thread: run blocking code in a worker thread, await the result
result = await asyncio.to_thread(legacy_report)
print(result)
# timeout guard — production habit for every external await
try:
await asyncio.wait_for(asyncio.sleep(10), timeout=1.0)
except TimeoutError:
print("gave up after 1s")
asyncio.run(main())
# Ecosystem: requests → httpx.AsyncClient, psycopg2 → asyncpg,
# time.sleep → asyncio.sleep. FastAPI async def endpoints live on this loop.Key Points to Remember
- 1async def defines a coroutine; it runs only when awaited; asyncio.run() starts the loop
- 2gather/create_task overlap I/O waits — total time ≈ the slowest task
- 3Never block the loop: no time.sleep, no requests.get inside coroutines
- 4asyncio.to_thread bridges blocking libraries; wait_for adds timeouts
Interview Questions
Sign in to ask AriaWhat happens if you call time.sleep(5) inside an async FastAPI endpoint?
Difference between await coro(), create_task(), and gather()?
asyncio is single-threaded — how does it handle 10,000 concurrent connections?
Ask Aria about asyncio — async/await and the Event Loop
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.