Mixing blocking calls into async code is the most common reason asyncio programs are slow.
asyncio runs all coroutines on one thread. They take turns, and a coroutine only gives up its turn at an await that actually waits. This is cooperative multitasking.
await asyncio.sleep(1) tells the loop "wake me in a second" and returns control immediately, so other coroutines run in the meantime. time.sleep(1) is an ordinary blocking call: the thread stops, and with it the event loop and every other coroutine.
The same applies to any blocking work:
requests.get(...), synchronous database drivers,open().read()on a slow disk,- CPU-heavy loops such as hashing or parsing a huge file.
All of these stall the loop. Use async-native libraries (aiohttp or httpx for HTTP, asyncpg for PostgreSQL), or push the blocking call to a thread with asyncio.to_thread, which you will meet later.
The example runs three one-second waits concurrently with asyncio.gather, finishing in about one second total. With time.sleep it would take three seconds even though the code looks concurrent. asyncio.sleep(0) is a handy idiom: it yields to the loop once without waiting, letting other ready coroutines run.
Example
import asyncio, time
async def wait_async(n):
await asyncio.sleep(1) # yields to the loop
return n
async def wait_blocking(n):
time.sleep(1) # blocks the whole thread
return n
async def timed(fn):
t = time.perf_counter()
res = await asyncio.gather(fn(1), fn(2), fn(3))
print(fn.__name__, res, f"{time.perf_counter() - t:.1f}s")
asyncio.run(timed(wait_async)) # wait_async [1, 2, 3] 1.0s
asyncio.run(timed(wait_blocking)) # wait_blocking [1, 2, 3] 3.0sRemember
- Coroutines only switch at await points that really wait
- Blocking calls (time.sleep, requests, heavy CPU) freeze every coroutine
- Use async libraries or asyncio.to_thread for blocking work
Watch out for
- Calling a synchronous HTTP client inside async def and wondering why nothing runs concurrently.