How asyncio works

Concurrency in Python services Mid-level Python 3.13+ PostgreSQL 17+ Reviewed By Best Latest Apps

Predict the output

Three 1-second sleeps run concurrently. Roughly how long does it take?

Python 3.13
import asyncio
import time


async def main() -> None:
    t = time.perf_counter()
    async with asyncio.TaskGroup() as tg:
        for _ in range(3):
            tg.create_task(asyncio.sleep(1))
    print(round(time.perf_counter() - t))


asyncio.run(main())

Options
Hint 1 of 1

Do the sleeps wait one after another?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A 1

Correct. The three sleeps wait at the same time on one loop.

asyncio.sleep gives control back to the event loop, so all three timers wait together. The TaskGroup exits once the last finishes, after about 1 second in total.

Why not the others?
  • B 3. That's sequential awaiting.
  • C 0. Each sleep still takes a second.
  • D It raises: TaskGroup needs results. Tasks can return None.

30-second answer

asyncio runs coroutines on an event loop. When a coroutine awaits I/O, it yields control, the loop registers interest with the OS (epoll or kqueue), and runs other ready coroutines. When data arrives the waiting coroutine resumes. One thread can juggle thousands of mostly idle connections because waiting costs almost nothing. Anything that doesn't await, like CPU work or a blocking library call, stalls every coroutine on that loop.

Practise saying it · 35 s. No recording, just a timer

Explain the event loop.

Say your answer out loud, then check which of these points you covered:

  • Coroutines yield at await
  • Loop waits on OS readiness
  • One thread, many connections
  • Blocking calls stall all
  • Async drivers end to end

Deep dive

await is the only place a coroutine can be paused; between awaits, code runs uninterrupted.

Use async drivers (psycopg async, asyncpg, httpx, redis.asyncio) end to end.

asyncio.TaskGroup (3.11+) runs tasks concurrently and cancels the rest if one fails.

Each request still costs memory; thousands of connections need a bounded pool to the database.

Common mistakes

  • Calling time.sleep() or a blocking HTTP client inside a coroutine.
  • Creating tasks and never awaiting them (lost exceptions).
  • Assuming async makes CPU work faster.

Senior take & follow-ups

Explain yield at await, the loop and OS readiness, and the blocking-call trap.

Follow-up questions

Think of your answer first, then open the hint.

  1. Why is asyncio good for I/O?

    Show hint

    Waiting coroutines cost little and don't need threads.

  2. TaskGroup or gather?

    Show hint

    TaskGroup cancels siblings on failure and raises an ExceptionGroup.

Suggest a fix if something here is wrong or out of date.