Python asyncio.wait_for: Timeouts and Cancellation
python asyncio wait_for: Learn how asyncio.wait_for applies timeouts to coroutines, handles cancellation, and raises TimeoutError in Python async programs.
When a coroutine may take longer than expected, asyncio.wait_for is the standard way to impose a deadline. It runs an awaitable and raises TimeoutError if the deadline passes, while cancelling the underlying task. This article explains how python asyncio wait_for behaves, how to handle cancellation, and where it fits in real async code.
What asyncio.wait_for Does
asyncio.wait_for(aw, timeout) takes an awaitable and a timeout in seconds. If timeout is None, it waits indefinitely. Otherwise, it waits up to the given number of seconds for the awaitable to complete. If the awaitable finishes first, its result is returned. If the timeout expires first, the underlying task is cancelled and TimeoutError is raised.
Here is a minimal example:
import asyncio async def slow_operation(): await asyncio.sleep(10) return "done" async def main(): try: result = await asyncio.wait_for(slow_operation(), timeout=2) print(result) except asyncio.TimeoutError: print("Operation timed out") asyncio.run(main())
The slow_operation coroutine is wrapped in a task automatically. After two seconds, wait_for cancels that task and raises TimeoutError. The except block catches it and the program continues.
How TimeoutError Is Raised and Handled
TimeoutError is the exception you catch when the deadline passes. In current Python versions, asyncio.TimeoutError is an alias for the built-in TimeoutError, so either name works. The exception is raised after the inner task has been cancelled. That means by the time you see it, the task may have already run its cancellation cleanup.
async def main(): try: await asyncio.wait_for(slow_operation(), timeout=1) except TimeoutError: print("Deadline exceeded")
You should handle TimeoutError at the point where the timeout is meaningful. If you need to distinguish a timeout from other failures, catch it before a general Exception handler.
Cancellation Behavior: What Happens When the Timeout Fires
When the timeout fires, wait_for cancels the inner task. Cancellation is cooperative: the coroutine receives a CancelledError at its current await point. If the coroutine does not suppress that exception, it stops and wait_for raises TimeoutError.
If the coroutine catches CancelledError and does not re-raise it, wait_for will wait for the coroutine to finish. This can turn a timeout into an indefinite wait if the cleanup code blocks. Use try/finally to release resources without swallowing the cancellation.
async def resource_heavy(): try: await asyncio.sleep(10) finally: # Close connections, release locks, etc. print("Releasing resources")
The finally block runs whether the coroutine completes normally or is cancelled. The CancelledError still propagates, so wait_for can raise TimeoutError as expected.
Passing a Coroutine vs. an Existing Task
wait_for accepts any awaitable. If you pass a coroutine object, it is wrapped in a new task. If you pass an existing task, that task is used directly. The practical difference is ownership.
async def main(): task = asyncio.create_task(slow_operation()) try: await asyncio.wait_for(task, timeout=1) except asyncio.TimeoutError: print("Timed out")
When you pass a task, you keep a reference to it. That can be useful if you need to inspect its state later. But if the task is cancelled by wait_for, awaiting it again later will raise CancelledError. Passing a coroutine is usually simpler because wait_for manages the task lifecycle for you.
Practical Example: Timeout for a Network Request
A common use is placing a timeout around an I/O operation that may hang. This pattern works with any async function, including HTTP clients, database drivers, or socket reads.
async def fetch_data(): # Simulate a slow network call await asyncio.sleep(5) return {"status": "ok"} async def main(): try: data = await asyncio.wait_for(fetch_data(), timeout=3) print(data) except asyncio.TimeoutError: print("Request timed out")
If you need to run several operations with individual timeouts, you can combine wait_for with asyncio.gather:
async def main(): tasks = [ asyncio.wait_for(fetch_data(), timeout=2), asyncio.wait_for(fetch_data(), timeout=2), ] results = await asyncio.gather(*tasks, return_exceptions=True)
return_exceptions=True lets you inspect each result or exception separately instead of letting the first failure cancel the others.
Common Mistakes and Edge Cases
wait_for only works with awaitables. Wrapping a blocking function like time.sleep does not help because it blocks the event loop and prevents the timeout from being checked. Use asyncio.sleep or run blocking code in a separate thread with asyncio.to_thread.
A timeout of 0 cancels the task immediately. This is rarely useful outside tests. A timeout of None disables the deadline entirely, which is the same as not using wait_for.
Another mistake is catching CancelledError inside the coroutine and not re-raising it. As mentioned, that can make wait_for wait longer than the timeout. Always let cancellation propagate unless you have a specific reason to suppress it.
Alternatives: asyncio.timeout and asyncio.timeout_at
Python 3.11 introduced asyncio.timeout, an async context manager that applies a timeout to a block of code. It is a good alternative when you want to bound multiple awaits without wrapping each one in wait_for.
async def main(): try: async with asyncio.timeout(2): await slow_operation() await another_operation() except TimeoutError: print("Block timed out")
asyncio.timeout_at accepts an absolute deadline instead of a duration. These context managers cancel the current task rather than creating a separate task, which can be slightly lighter than wait_for for a single coroutine. wait_for remains the right choice when you need the result of one specific awaitable and want a simple try/except around it.
Performance and Resource Considerations
wait_for creates a new task when given a coroutine. That task has overhead, and the cancellation mechanism requires the coroutine to be cancellable. If you apply wait_for to a large number of short operations, the task creation cost may dominate. In that case, prefer asyncio.timeout for a block of code or design the operation to accept a timeout parameter directly.
Cancellation also has resource implications. A coroutine that ignores CancelledError can keep file handles, connections, or locks open after the timeout. Always use try/finally or async with for resources so cleanup runs during cancellation. This keeps the timeout behavior predictable and prevents leaks in long-running services.