Concurrency vs blocking

asyncio · Essentials · 9 min

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.0s

Remember

  • 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.

Check yourself

  1. Three coroutines each call time.sleep(1) and are run with gather. Roughly how long does it take?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/asyncio-task.html#sleeping