Timeouts and Cancellation
30 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Real services are sometimes slow or never answer. A program that waits forever is as broken as one that crashes, so every wait needs a limit.
Stop waiting after a time limit
Timeout is a limit on how long you will wait. In asyncio, when the limit passes, the waiting work is cancelled and TimeoutError is raised to your code.
A slow service can hold your program forever. asyncio.wait_for(coro, timeout) waits at most that many seconds, then cancels the work and raises TimeoutError.
import asyncio
async def slow(): await asyncio.sleep(1)
async def main(): try: await asyncio.wait_for(slow(), timeout=0.1) except TimeoutError: print("too slow")
asyncio.run(main())Catch it and decide what to do.
Example 1 · wait_for stops a slow call
import asyncio
async def fetch(): await asyncio.sleep(1) # a slow service return "data"
async def main(): try: print(await asyncio.wait_for(fetch(), timeout=0.1)) except TimeoutError: print("timed out")
asyncio.run(main())timed out
What this code does
- The call would take 1 second, but we only wait 0.1 s.
wait_forcancelsfetch()and raisesTimeoutError, which we catch.
Example 2 · One timeout for a block
import asyncio
async def step(n): await asyncio.sleep(n) return n
async def main(): try: async with asyncio.timeout(0.15): print(await step(0.01)) print(await step(0.5)) except TimeoutError: print("block timed out")
asyncio.run(main())0.01 block timed out
What this code does
- The limit of 0.15 s covers both awaits together.
- The first step is quick and prints. The second runs past the limit, so the block ends with
TimeoutError.
Python 3.11 added async with asyncio.timeout(0.1):, which puts one limit on a whole block of awaits.
Cancelling a task
Cancellation is a request to stop a task. asyncio raises CancelledError inside the task at its next await, so the task can clean up before it ends.
task.cancel() asks a task to stop. At its next await, the task gets a CancelledError, which ends it unless the code handles it.
async def main(): task = asyncio.create_task(worker()) await asyncio.sleep(0) # let it start task.cancel() try: await task except asyncio.CancelledError: print(task.cancelled())Await the task afterwards to see it finish.
Cancelling is a request, not a kill. The task stops at an await, so a task stuck in a long loop with no await cannot be cancelled.
Clean up with finally
When a task is cancelled or times out, it may hold a connection or a lock. Put the release in finally, which always runs.
async def worker(log): try: log.append("start") await asyncio.sleep(10) finally: log.append("cleanup")Do not catch CancelledError and carry on. Let it go on, or re-raise it.
Example 3 · Cancel a task and clean up
import asyncio
async def worker(log): try: log.append("start") await asyncio.sleep(10) finally: log.append("cleanup")
async def main(): log = [] task = asyncio.create_task(worker(log)) await asyncio.sleep(0) # let the worker start task.cancel() try: await task except asyncio.CancelledError: log.append("cancelled") print(log, task.cancelled())
asyncio.run(main())['start', 'cleanup', 'cancelled'] True
What this code does
- The worker starts, then waits for 10 seconds.
cancel()raisesCancelledErrorinside it at theawait, sofinallyruns the cleanup.- Awaiting the cancelled task re-raises the error, which we catch.
If you swallow CancelledError, timeouts and task groups stop working correctly, because they depend on the cancel reaching the top.
Retry and first-wins patterns
A timeout per attempt, plus a short loop, gives a retry. asyncio.wait with FIRST_COMPLETED gives the fastest of several sources.
Cancel the losers so they stop using resources.
Example 4 · Retry with a timeout per attempt
import asyncio
calls = {"n": 0}
async def flaky(): calls["n"] += 1 await asyncio.sleep(0.5 if calls["n"] < 3 else 0.01) return "ok"
async def main(): for attempt in range(1, 4): try: result = await asyncio.wait_for(flaky(), timeout=0.1) print(result, "after", attempt, "attempts") return except TimeoutError: print("attempt", attempt, "timed out")
asyncio.run(main())attempt 1 timed out attempt 2 timed out ok after 3 attempts
What this code does
- The first two attempts are slow, so each is cancelled at 0.1 s.
- The third answers fast and the loop returns.
Example 5 · First answer wins
import asyncio
async def source(name, delay): await asyncio.sleep(delay) return name
async def main(): tasks = [ asyncio.create_task(source("cache", 0.01)), asyncio.create_task(source("database", 0.5)), ] done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) for task in pending: task.cancel() print([t.result() for t in done], len(pending))
asyncio.run(main())['cache'] 1
What this code does
waitreturns as soon as one task finishes.- The slower task is still pending, so we cancel it to stop it using resources.
Both patterns are built from the same pieces: tasks, wait_for and cancel. Keep attempts few, and add a pause between retries in real services.
Next step
Check your understanding
10 quick questions