How do you cancel or add a timeout to an asyncio task safely?

6 minadvancedasynciocancellationtimeout

Quick Answer

Use `asyncio.wait_for(coro, timeout=...)` to raise `TimeoutError` if a coroutine doesn't finish in time (it cancels the underlying task internally), or call `task.cancel()` directly to request cancellation, which raises `asyncio.CancelledError` **inside** the task at its next `await` point. Always let `CancelledError` propagate (don't swallow it with a bare `except Exception`) unless you specifically need to run cleanup first — bare-catching it breaks cancellation and can hang shutdown.

Detailed Answer

Timeouts with asyncio.wait_for

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)
    except TimeoutError:
        print("timed out after 2 seconds")

asyncio.run(main())

wait_for runs the coroutine as a task and races it against the timeout; if the timeout elapses first, it cancels the task internally and raises TimeoutError (in Python 3.11+; asyncio.TimeoutError, an alias of the same, in earlier versions) to the caller.

Python 3.11+'s asyncio.timeout(): a context-manager alternative

async def main():
    try:
        async with asyncio.timeout(2):
            await slow_operation()
    except TimeoutError:
        print("timed out")

asyncio.timeout() applies a deadline to an entire async with block (potentially covering multiple awaits), which is more flexible than wait_for when you want one timeout to cover several sequential operations rather than wrapping each individually.

Explicit cancellation with task.cancel()

async def worker():
    try:
        await asyncio.sleep(100)
    except asyncio.CancelledError:
        print("cleaning up before exiting")
        raise             # IMPORTANT: re-raise so the cancellation actually completes

async def main():
    task = asyncio.create_task(worker())
    await asyncio.sleep(1)
    task.cancel()          # requests cancellation
    await task              # propagates CancelledError here, after cleanup runs

task.cancel() doesn't stop the task immediately — it arranges for CancelledError to be raised inside the task at its next await point. The task can catch that to run cleanup (closing a connection, releasing a lock), but must re-raise it afterward; swallowing CancelledError silently prevents the cancellation from actually taking effect and can leave .cancel() callers waiting forever.

Why you must never blanket-catch CancelledError

async def bad_worker():
    try:
        await asyncio.sleep(100)
    except Exception:        # BUG: in Python 3.8+, CancelledError is a
        pass                  # BaseException subclass, NOT caught here --
                                # but a bare `except:` WOULD catch it and break cancellation

Since Python 3.8, asyncio.CancelledError inherits from BaseException (not Exception), specifically so that ordinary except Exception: blocks don't accidentally swallow it — but a bare except: still would, so it's a rule worth stating explicitly: always let cancellation propagate unless you're deliberately intercepting it to clean up, and always re-raise afterward.

Interview-ready summary: Use asyncio.wait_for/asyncio.timeout() for deadline-based cancellation, and task.cancel() for explicit cancellation — both work by raising CancelledError inside the task at its next await. Treat CancelledError as something to clean up around, never to swallow, since eating it silently breaks the cancellation contract for whoever is waiting on the task.