Python Async Timeout Handling — Core Concepts
Why Timeouts Matter in Async Code
Async programs typically coordinate with external services — APIs, databases, message queues. Any of these can become unresponsive. Without timeouts, a single stalled coroutine can hold resources indefinitely, eventually degrading the entire application.
Timeouts are a contract: “I will wait this long, and no longer.”
The Three Main Approaches
1. asyncio.wait_for (Python 3.4+)
The oldest and most common approach:
import asyncio
async def fetch_data():
await asyncio.sleep(10) # Simulates a slow operation
return "data"
async def main():
try:
result = await asyncio.wait_for(fetch_data(), timeout=3.0)
except asyncio.TimeoutError:
print("Operation timed out")
wait_for wraps a coroutine and cancels it if the timeout expires. The coroutine receives a CancelledError, and the caller gets a TimeoutError.
2. asyncio.timeout Context Manager (Python 3.11+)
The modern approach, using structured cancellation:
async def main():
try:
async with asyncio.timeout(3.0):
result = await fetch_data()
more = await process(result)
except TimeoutError:
print("The whole block timed out")
The context manager can wrap multiple operations. If the total time exceeds 3 seconds — across all awaits inside the block — it cancels.
3. asyncio.timeout_at (Python 3.11+)
Like timeout, but takes an absolute deadline instead of a relative duration:
async def main():
deadline = asyncio.get_event_loop().time() + 5.0
try:
async with asyncio.timeout_at(deadline):
await step_one()
await step_two() # Combined must finish by deadline
except TimeoutError:
print("Missed the deadline")
This is useful when multiple stages share a single deadline.
How Cancellation Works Under the Hood
When a timeout fires, asyncio doesn’t kill the coroutine. It raises CancelledError inside it at the next await point. The coroutine can catch this to run cleanup:
async def graceful_operation():
conn = await connect()
try:
await conn.execute("LONG QUERY")
except asyncio.CancelledError:
await conn.rollback() # Clean up before dying
raise # Always re-raise CancelledError
Critical rule: Never swallow CancelledError. If you catch it for cleanup, always re-raise it. Swallowing it breaks the timeout mechanism.
Choosing Between Approaches
| Feature | wait_for | timeout context manager |
|---|---|---|
| Python version | 3.4+ | 3.11+ |
| Wraps | Single coroutine | Block of code |
| Reschedule timeout | No | Yes (.reschedule()) |
| Structured cancellation | No | Yes |
For new code on Python 3.11+, prefer the context manager. It’s cleaner, supports multiple operations, and integrates better with structured concurrency.
Common Misconception
“A timeout means the operation completed in time.” Not exactly. When a timeout fires, the cancelled operation might have partially completed side effects (sent half a message, written some rows). Timeouts cancel execution, but they can’t undo work already done. Design your operations to be idempotent when timeouts are involved.
One thing to remember: Python 3.11’s asyncio.timeout() context manager is the modern way to handle timeouts — it can wrap multiple operations under one deadline and integrates with structured cancellation. Always re-raise CancelledError after cleanup.
See Also
- Python Actor Model Why treating each piece of your program like a person with their own mailbox makes concurrency way less scary.
- Python Aiocache Caching aiocache remembers expensive answers so your async Python app doesn't waste time asking the same question twice.
- Python Aiofiles Async Io aiofiles lets your async Python program read and write files without freezing — because normal file operations secretly block everything.
- Python Aiohttp Understand Aiohttp through an everyday analogy so Python behavior feels intuitive, not random.
- Python Anyio Portability AnyIO lets your async Python code work with any async library — write once, run on asyncio or Trio without changes.