Python asyncio.timeout: Control Deadlines

Published on: August 28, 2026
Reading time: 4 minutes
A developer typing code on a laptop with a Python book beside in an office.

Asynchronous operations need limits. A network call may never respond, a queue may remain empty, and a slow dependency may consume the entire request budget. asyncio.timeout() creates a deadline scope: if the block does not finish in time, the current task is cancelled and the context converts that cancellation into TimeoutError.

This guide covers relative and absolute deadlines, rescheduling, expiration checks, TaskGroup integration, comparison with asyncio.wait_for(), cleanup, shielding, retries, and common cancellation mistakes.

Your first timeout

import asyncio

async def slow_operation() -> str:
    await asyncio.sleep(5)
    return "ok"

async def main():
    try:
        async with asyncio.timeout(1.0):
            result = await slow_operation()
            print(result)
    except TimeoutError:
        print("deadline exceeded")

asyncio.run(main())

After one second, the context cancels the current task. When the block exits, the internal cancellation is transformed into TimeoutError, which should be caught outside the context.

Why catch outside the block?

The conversion from CancelledError to TimeoutError happens when the context manager exits.

try:
    async with asyncio.timeout(1.0):
        await operation()
except TimeoutError:
    ...

This also makes it clear which scope owns the deadline.

Timeout cannot interrupt blocking code

Asyncio cancellation is cooperative. A timeout can act only when the coroutine yields control to the event loop. CPU-bound code or a direct blocking call prevents timely cancellation.

async def incorrect():
    time.sleep(10)  # blocks the event loop

Use asyncio.to_thread(), an executor, or a process for blocking work.

async with asyncio.timeout(2.0):
    result = await asyncio.to_thread(blocking_function)

Cancelling the await stops waiting for the thread, but it does not necessarily stop the underlying function. Give blocking libraries their own native timeout when possible.

Relative timeout and absolute deadline

asyncio.timeout(seconds) defines a relative limit. asyncio.timeout_at(when) receives an absolute value from the event loop’s monotonic clock.

loop = asyncio.get_running_loop()
deadline = loop.time() + 3.0

async with asyncio.timeout_at(deadline):
    await step_a()
    await step_b()

Absolute deadlines are useful when several layers must share one budget rather than restarting the timer.

Propagating a time budget

If a request has five seconds total, each nested function should not receive a new five-second allowance.

async def query_a(deadline: float):
    async with asyncio.timeout_at(deadline):
        return await call_a()

async def flow():
    loop = asyncio.get_running_loop()
    deadline = loop.time() + 5.0
    a = await query_a(deadline)
    b = await query_b(deadline)
    return a, b

Time spent in the first step automatically reduces what remains for the second.

Rescheduling a timeout

The timeout object exposes reschedule(). This is useful when the deadline becomes known only after reading metadata or negotiating with another service.

async def process():
    loop = asyncio.get_running_loop()

    async with asyncio.timeout(None) as control:
        limit = await get_limit()
        control.reschedule(loop.time() + limit)
        return await run_work()

None creates a context without an initial deadline. Rescheduled times use the loop’s monotonic clock.

Checking expiration

After the context, expired() reports whether the deadline was reached.

control = None
try:
    async with asyncio.timeout(1.0) as control:
        await work()
except TimeoutError:
    pass

if control is not None and control.expired():
    print("the deadline expired")

Catching TimeoutError is normally enough; expired() is useful for metrics and diagnostics.

Nested timeouts

Timeout contexts may be safely nested. The shorter deadline fires first.

async with asyncio.timeout(10.0):
    await fast_step()
    try:
        async with asyncio.timeout(1.0):
            await optional_call()
    except TimeoutError:
        use_fallback()
    await final_step()

The inner timeout can be handled locally while the outer timeout protects the total request budget.

Combining timeout with TaskGroup

A timeout around TaskGroup applies to the whole concurrent unit.

async def dashboard():
    try:
        async with asyncio.timeout(3.0):
            async with asyncio.TaskGroup() as group:
                user = group.create_task(get_user())
                orders = group.create_task(get_orders())
    except TimeoutError:
        return {"error": "services were slow"}

    return {
        "user": user.result(),
        "orders": orders.result(),
    }

When the deadline expires, the containing task is cancelled; TaskGroup cancels children and waits for cleanup.

Per-task limits inside a group

Put a timeout inside each child when operations have separate limits.

async def query_with_limit(name: str, seconds: float):
    async with asyncio.timeout(seconds):
        return await query(name)

If TimeoutError escapes, TaskGroup treats it as a failure and cancels siblings. If timeout is an expected result, catch it inside the child and return an explicit status.

Comparison with asyncio.wait_for

asyncio.wait_for(awaitable, timeout) wraps one awaitable. asyncio.timeout() wraps an entire block with multiple awaits and intermediate logic.

result = await asyncio.wait_for(operation(), timeout=2.0)

async with asyncio.timeout(2.0):
    header = await read_header()
    body = await read_body(header)
    await validate(body)

The context manager is often clearer for composed flows. wait_for() remains convenient for one operation.

Cleanup and finally

Cancelled coroutines must release resources.

async def use_connection():
    connection = await open_connection()
    try:
        return await connection.receive()
    finally:
        await connection.close()

The timeout waits while cancellation propagates and cleanup runs. A slow cleanup can make observed duration exceed the nominal deadline. Keep shutdown bounded and predictable.

Do not swallow CancelledError

Inside the operation, timeout is implemented with cancellation. Catching CancelledError and returning normally may prevent the timeout context from recognizing expiration.

async def bad():
    try:
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        return "ignored"

Perform cleanup and re-raise.

except asyncio.CancelledError:
    await cleanup()
    raise

Shielding critical work

asyncio.shield() can protect an inner task from cancellation, but ownership must remain explicit.

task = asyncio.create_task(write_audit())
try:
    async with asyncio.timeout(1.0):
        await asyncio.shield(task)
except TimeoutError:
    ...

await task

The timeout stops waiting, but the protected task continues. Keep a strong reference and define who will await it. Shield can otherwise recreate detached tasks.

Timeout is not retry

A timeout ends one slow attempt. Retry policy is separate and must define attempts, backoff, jitter, idempotency, and the total budget.

async def attempt(deadline: float):
    for index in range(3):
        try:
            async with asyncio.timeout_at(deadline):
                return await call()
        except TimeoutError:
            if index == 2:
                raise
            await asyncio.sleep(0.1 * (2 ** index))

The absolute deadline prevents retries from exceeding the request budget.

Testing timeout behavior

Avoid tests with long real sleeps. Use short but reasonable limits, asyncio.Event, and fakes that complete only after a signal.

async def never(event: asyncio.Event):
    await event.wait()

async def test_timeout():
    event = asyncio.Event()
    try:
        async with asyncio.timeout(0.05):
            await never(event)
    except TimeoutError:
        pass

Do not assert exact millisecond timing in CI. Test behavior: exception, cleanup, and cancellation of related tasks.

Metrics and observability

Distinguish connection timeout, read timeout, queue timeout, operation timeout, and total deadline. Record elapsed time, remaining budget, dependency name, and request ID.

A high timeout rate may indicate saturation, a slow dependency, an overly aggressive deadline, or a blocked event loop.

Common mistakes

  • Catching TimeoutError inside the context: conversion occurs on exit.
  • Calling time.sleep in async code: the event loop is blocked.
  • Swallowing CancelledError: timeout semantics may break.
  • Restarting the budget in each layer: prefer absolute deadlines.
  • Expecting timeout to kill a thread: async cancellation cannot stop arbitrary native work.
  • Using shield without ownership: work may continue unsupervised.

Complete example: shared service budget

import asyncio

async def query_service(name: str, deadline: float) -> dict:
    async with asyncio.timeout_at(deadline):
        await asyncio.sleep(0.2)
        return {"service": name, "ok": True}

async def aggregate() -> list[dict]:
    loop = asyncio.get_running_loop()
    deadline = loop.time() + 2.0
    tasks: list[asyncio.Task[dict]] = []

    async with asyncio.timeout_at(deadline):
        async with asyncio.TaskGroup() as group:
            for name in ["profile", "orders", "balance"]:
                tasks.append(
                    group.create_task(
                        query_service(name, deadline),
                        name=f"query-{name}",
                    )
                )

    return [task.result() for task in tasks]

Every operation shares one deadline and belongs to one structured group. The flow does not create a fresh budget for each dependency.

Conclusion

asyncio.timeout() turns a deadline into a clear scope. It uses cooperative cancellation, converts expiration into TimeoutError, and can protect multiple related awaits.

The official Python asyncio timeout documentation covers timeout(), timeout_at(), rescheduling, and expiration. Use absolute deadlines for shared budgets, preserve cleanup, and treat timeout as one part of a broader resilience policy.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Code on a screen representing Python t-strings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python 3.14 T-strings: Structured Templates

    Learn Python t-strings in Python 3.14, inspect Template and Interpolation objects, and build structured, extensible renderers.

    Ler mais

    Tempo de leitura: 5 minutos
    16/10/2026
    CPU and memory modules representing Python shared_memory track
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    shared_memory track: Control the Resource Tracker

    Learn Python shared_memory track for safe resource tracking, ownership, cleanup, and independent process coordination.

    Ler mais

    Tempo de leitura: 5 minutos
    16/10/2026
    Data storage unit representing compression with Python compression.zstd
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    compression.zstd: Streaming and Safe Compression

    Learn Python compression.zstd for fast file and data compression with streaming, dictionaries, testing, and safe limits.

    Ler mais

    Tempo de leitura: 6 minutos
    15/10/2026
    Code on screen for diagnosing processes with Python sys.remote_exec
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    sys.remote_exec: Diagnose Running Python Processes

    Learn Python sys.remote_exec for diagnosing running processes with controlled scripts, strong permissions, and safe operational practices.

    Ler mais

    Tempo de leitura: 6 minutos
    15/10/2026
    Python code and environment configuration with os.reload_environ
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    os.reload_environ: Thread Safety and Environment

    Learn Python os.reload_environ to refresh externally changed environment variables with validation, compatibility, and thread-safety precautions.

    Ler mais

    Tempo de leitura: 4 minutos
    14/10/2026
    Python code representing contextvars.Token and asynchronous context isolation
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    contextvars.Token: Restore Async Context Safely

    Learn Python contextvars.Token for temporary values, automatic restoration, and safe context isolation across asynchronous tasks.

    Ler mais

    Tempo de leitura: 6 minutos
    14/10/2026