asyncio.sleep() vs. time.sleep(), and create_task()

time.sleep() inside a coroutine blocks everything

This is the second “show the pain” moment worth taking seriously: using time.sleep() — the regular, blocking version — inside an async def function doesn’t just fail to be concurrent for that one call. It blocks the entire event loop, meaning nothing else scheduled through asyncio can make progress either, even other coroutines running alongside it in the same gather():

import asyncio
import time

async def call_search_api():
    print("calling search API...")
    time.sleep(2)   # blocking — not await asyncio.sleep(2)
    return "3 results found"

async def call_weather_api():
    print("calling weather API...")
    await asyncio.sleep(1)
    return "72F and sunny"

async def main():
    return await asyncio.gather(call_search_api(), call_weather_api())

start = time.perf_counter()
results = asyncio.run(main())
elapsed = time.perf_counter() - start
print(f"total time: {elapsed:.1f}s")
calling search API...
calling weather API...
total time: 3.0s

(illustrating expected behavior on a real machine — this demo relies on time.sleep()’s blocking behavior, which isn’t reliable in this particular in-browser sandbox; see the callout at the end of this section)

Roughly 3.0 seconds — 2 + 1, not the ~2.0 seconds you’d expect from gather() running both concurrently. What happened: call_search_api() started, then hit time.sleep(2) — a call that has no idea an event loop even exists, and simply freezes the entire program for two seconds, call_weather_api() included, even though call_weather_api() itself was written correctly with await asyncio.sleep(1). One incorrectly blocking call inside an async function poisons the concurrency for everything else sharing that event loop, not just its own coroutine.

The fix is simply using await asyncio.sleep(...) everywhere a coroutine needs to pause — never the plain, blocking time.sleep(). This is the practical rule worth internalizing: inside async def code, a blocking call of any kind (a blocking sleep, a blocking network request from a non-async library, blocking disk I/O) has this same poisoning effect on everything else scheduled to run concurrently.

Callout: the same in-browser sandbox caveat from Concept 1 applies here, doubled — this demo’s whole point is time.sleep()’s blocking behavior, which this particular sandbox doesn’t reliably reproduce. The output shown above is what you’d see running this code locally; treat this section as the specific case where you should mentally run the code on a real machine rather than trusting whatever the sandbox actually shows when you hit Run.

create_task() — starting something now, awaiting it later

asyncio.gather() fits the common case: start several things and wait for all of them together, right away. Sometimes you want to start a coroutine running in the background, do something else in the meantime, and only collect its result later. asyncio.create_task() does exactly that — it schedules a coroutine to start running immediately, and hands you back a Task object you can await whenever you’re actually ready for its result:

Try it — edit and run

Notice "calling search API..." prints immediately — create_task() starts the coroutine right away, not when it’s later await-ed. call_search_api()’s 2-second wait is already well underway during the 0.5-second “other setup work,” so by the time await task actually runs, much of the real waiting has already happened concurrently, in the background. await task at the end just collects the result whenever it’s actually needed — the task itself started running the moment create_task() was called.

gather() and create_task() solve overlapping but distinct problems: gather() is the right tool when you already have every coroutine you need and just want them all run together, all at once. create_task() fits when you want to kick something off before you’re ready to wait for it, potentially doing unrelated work — synchronous or otherwise — in between starting it and actually needing its result.

Check your understanding
1/5

What happens if a coroutine's body calls the blocking time.sleep() instead of await asyncio.sleep()?