What are common asyncio pitfalls?
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.