What are common asyncio pitfalls?

7 minadvancedasynciogotchaspitfalls

Quick Answer

The most common ones: calling a **blocking** (synchronous) function inside a coroutine without offloading it, which freezes the entire event loop, not just that task; forgetting to `await` a coroutine (creating it but never running it, which raises a "coroutine was never awaited" warning); and creating tasks with `asyncio.create_task()` but never keeping a reference or awaiting them, letting them be garbage-collected mid-execution or silently swallow exceptions.

Detailed Answer

Pitfall 1: blocking calls freeze the whole event loop

import asyncio
import time

async def bad():
    time.sleep(5)     # BLOCKS the entire event loop -- every other task stalls too!

async def good():
    await asyncio.sleep(5)   # yields control -- other tasks keep running

time.sleep (or any synchronous, blocking I/O call, or CPU-heavy computation) doesn't await anything, so the event loop has no opportunity to run other coroutines while it executes — a single blocking call anywhere stalls every pending task, not just the one that made it. Offload genuinely blocking/synchronous work with loop.run_in_executor(None, blocking_func) instead.

Pitfall 2: creating a coroutine without awaiting it

async def fetch():
    ...

async def main():
    fetch()          # BUG: creates a coroutine object but never runs it!
    # RuntimeWarning: coroutine 'fetch' was never awaited

Calling fetch() just constructs a coroutine object — it does nothing until awaited, scheduled as a task, or passed to asyncio.gather(). Forgetting the await is one of the most common asyncio bugs, and Python does emit a RuntimeWarning for it, but it's easy to miss in noisy logs.

Pitfall 3: fire-and-forget tasks losing their reference

async def main():
    asyncio.create_task(background_job())   # BUG: no reference kept!
    await asyncio.sleep(10)
    # background_job's task can be garbage-collected before it finishes,
    # silently cancelling it, and any exception it raised is never surfaced
async def main():
    task = asyncio.create_task(background_job())   # keep a reference
    try:
        await asyncio.sleep(10)
    finally:
        await task   # ensure it completes, and its exceptions propagate

The asyncio docs explicitly warn: "Save a reference to the result of this function, to avoid a task disappearing mid-execution." Without a kept reference, the event loop has no obligation to keep the task alive, and exceptions raised inside an un-awaited, unreferenced task are simply logged (via asyncio's default exception handler) rather than raised anywhere your code can catch them.

Pitfall 4: mixing asyncio.run() calls / event loop confusion

async def main():
    ...

asyncio.run(main())
asyncio.run(main())   # fine -- each call creates and closes its own loop

# But calling asyncio.run() from WITHIN a running coroutine is an error:
async def bad():
    asyncio.run(other_coro())   # RuntimeError: asyncio.run() cannot be called
                                  # from a running event loop

asyncio.run() is meant to be the single top-level entry point per program/script — nesting it inside already-running async code doesn't work; use await other_coro() instead.

Interview-ready summary: The recurring theme across asyncio pitfalls is forgetting that cooperative scheduling depends entirely on await points: blocking calls without an await freeze everything, coroutines created without await/create_task never run, and tasks created without a kept reference can vanish silently along with any exception they raised.