asyncio.timeout en Python: controla plazos

Publicado el: 28/08/2026
Tempo de leitura: 5 minutos
A man in a blue shirt holding a wall clock above his head, contemplating time.

Las operaciones asíncronas necesitan límites. Una llamada de red puede no responder, una cola puede permanecer vacía y una dependencia lenta puede consumir todo el presupuesto de una solicitud. asyncio.timeout() crea un ámbito con deadline: si el bloque no termina a tiempo, la tarea actual se cancela y el contexto convierte esa cancelación en TimeoutError.

Esta guía cubre deadlines relativos y absolutos, reagendamiento, comprobación de expiración, integración con TaskGroup, comparación con asyncio.wait_for(), cleanup, shield, reintentos y errores comunes de cancelación.

Primer timeout

import asyncio

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

async def main():
    try:
        async with asyncio.timeout(1.0):
            resultado = await operacion_lenta()
            print(resultado)
    except TimeoutError:
        print("plazo excedido")

asyncio.run(main())

Después de un segundo, el contexto cancela la tarea actual. Al salir del bloque, la cancelación interna se convierte en TimeoutError, que debe capturarse fuera del contexto.

Por qué capturar fuera

La transformación de CancelledError a TimeoutError ocurre cuando el context manager termina.

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

El patrón también deja claro qué ámbito posee el deadline.

Un timeout no interrumpe código bloqueante

La cancelación de asyncio es cooperativa. El timeout puede actuar únicamente cuando la coroutine entrega el control al event loop. Código CPU-bound o una llamada bloqueante directa impide reaccionar.

async def incorrecto():
    time.sleep(10)  # bloquea el event loop

Usa asyncio.to_thread(), un executor o un proceso.

async with asyncio.timeout(2.0):
    resultado = await asyncio.to_thread(funcion_bloqueante)

Cancelar el await deja de esperar la thread, pero no necesariamente detiene la función subyacente. Configura límites nativos en la biblioteca bloqueante cuando sea posible.

Timeout relativo y deadline absoluto

asyncio.timeout(segundos) define un límite relativo. asyncio.timeout_at(cuando) recibe un instante absoluto del reloj monotónico del loop.

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

async with asyncio.timeout_at(deadline):
    await etapa_a()
    await etapa_b()

Los deadlines absolutos son útiles cuando varias capas deben compartir un único presupuesto sin reiniciar el cronómetro.

Propagar un presupuesto de tiempo

Si una solicitud dispone de cinco segundos, cada función interna no debería recibir cinco segundos nuevos.

async def consultar_a(deadline: float):
    async with asyncio.timeout_at(deadline):
        return await llamada_a()

async def flujo():
    loop = asyncio.get_running_loop()
    deadline = loop.time() + 5.0
    a = await consultar_a(deadline)
    b = await consultar_b(deadline)
    return a, b

El tiempo gastado en la primera etapa reduce automáticamente lo disponible para la segunda.

Reagendar el timeout

El objeto del contexto ofrece reschedule(). Es útil cuando el deadline se conoce después de leer metadatos o negociar con otro sistema.

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

    async with asyncio.timeout(None) as control:
        limite = await obtener_limite()
        control.reschedule(loop.time() + limite)
        return await ejecutar_trabajo()

None crea un contexto inicialmente sin plazo. El nuevo instante usa el reloj monotónico del loop.

Comprobar expiración

Después del contexto, expired() indica si se alcanzó el deadline.

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

if control is not None and control.expired():
    print("el plazo expiró")

Capturar TimeoutError suele ser suficiente. expired() ayuda en métricas y diagnóstico.

Timeouts anidados

Los contextos pueden anidarse. El deadline más corto se activa primero.

async with asyncio.timeout(10.0):
    await etapa_rapida()
    try:
        async with asyncio.timeout(1.0):
            await llamada_opcional()
    except TimeoutError:
        usar_fallback()
    await etapa_final()

El timeout interno puede manejarse localmente y el externo sigue protegiendo el presupuesto total.

Combinar con TaskGroup

Un timeout alrededor de TaskGroup protege toda la unidad concurrente.

async def panel():
    try:
        async with asyncio.timeout(3.0):
            async with asyncio.TaskGroup() as grupo:
                usuario = grupo.create_task(obtener_usuario())
                pedidos = grupo.create_task(obtener_pedidos())
    except TimeoutError:
        return {"error": "servicios lentos"}

    return {
        "usuario": usuario.result(),
        "pedidos": pedidos.result(),
    }

Al expirar, se cancela la tarea contenedora; TaskGroup cancela las hijas y espera su cleanup.

Límites individuales dentro del grupo

Coloca un timeout dentro de cada hija cuando las operaciones tengan límites distintos.

async def consultar_con_limite(nombre: str, segundos: float):
    async with asyncio.timeout(segundos):
        return await consultar(nombre)

Si TimeoutError escapa, TaskGroup trata la tarea como fallida y cancela las hermanas. Si el timeout es esperado, captúralo dentro y devuelve un estado explícito.

Comparación con asyncio.wait_for

asyncio.wait_for(awaitable, timeout) envuelve un awaitable. asyncio.timeout() envuelve un bloque completo con varios awaits.

resultado = await asyncio.wait_for(operacion(), timeout=2.0)

async with asyncio.timeout(2.0):
    cabecera = await leer_cabecera()
    cuerpo = await leer_cuerpo(cabecera)
    await validar(cuerpo)

El context manager suele ser más claro para flujos compuestos. wait_for() sigue siendo cómodo para una operación.

Cleanup y finally

Las coroutines canceladas deben liberar recursos.

async def usar_conexion():
    conexion = await abrir_conexion()
    try:
        return await conexion.recibir()
    finally:
        await conexion.cerrar()

El timeout espera mientras la cancelación se propaga y el cleanup termina. Un cierre lento puede hacer que la duración real supere el plazo nominal. Mantén los cierres previsibles.

No ocultar CancelledError

Dentro de la operación, el timeout funciona mediante cancelación. Capturar CancelledError y retornar normalmente puede impedir que el contexto detecte la expiración.

async def malo():
    try:
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        return "ignorado"

Realiza cleanup y vuelve a lanzar.

except asyncio.CancelledError:
    await limpiar()
    raise

Shield para trabajo crítico

asyncio.shield() puede proteger una tarea interna de la cancelación, pero la propiedad debe quedar clara.

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

await tarea

El timeout deja de esperar, pero la tarea continúa. Conserva una referencia fuerte y define quién la esperará. Shield puede crear trabajo suelto si se usa sin supervisión.

Timeout no es retry

Un timeout termina un intento lento. La política de reintentos debe definir cantidad, backoff, jitter, idempotencia y presupuesto total.

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

El deadline absoluto impide que los reintentos superen el presupuesto de la solicitud.

Pruebas de timeout

Evita tests con pausas largas. Usa límites breves pero razonables, asyncio.Event y fakes controlados.

async def nunca(evento: asyncio.Event):
    await evento.wait()

async def prueba_timeout():
    evento = asyncio.Event()
    try:
        async with asyncio.timeout(0.05):
            await nunca(evento)
    except TimeoutError:
        pass

No verifiques milisegundos exactos en CI. Prueba el comportamiento: excepción, cleanup y cancelación de tareas relacionadas.

Métricas y observabilidad

Diferencia timeout de conexión, lectura, cola, operación y presupuesto total. Registra duración, presupuesto restante, dependencia e identificador de solicitud.

Una tasa elevada puede indicar saturación, una dependencia lenta, un límite demasiado agresivo o un event loop bloqueado.

Errores comunes

  • Capturar TimeoutError dentro del contexto: la conversión ocurre al salir.
  • Usar time.sleep en código async: bloquea el loop.
  • Ocultar CancelledError: rompe la semántica del timeout.
  • Reiniciar el presupuesto en cada capa: usa deadlines absolutos.
  • Esperar que el timeout mate una thread: la cancelación async no detiene trabajo nativo arbitrario.
  • Usar shield sin ownership: el trabajo puede continuar sin supervisión.

Ejemplo completo: presupuesto compartido

import asyncio

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

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

    async with asyncio.timeout_at(deadline):
        async with asyncio.TaskGroup() as grupo:
            for nombre in ["perfil", "pedidos", "saldo"]:
                tareas.append(
                    grupo.create_task(
                        consultar_servicio(nombre, deadline),
                        name=f"consulta-{nombre}",
                    )
                )

    return [tarea.result() for tarea in tareas]

Todas las operaciones comparten el mismo deadline y pertenecen al mismo grupo estructurado.

Conclusión

asyncio.timeout() convierte un deadline en un ámbito claro. Usa cancelación cooperativa, transforma la expiración en TimeoutError y puede proteger varias operaciones relacionadas.

La documentación oficial de timeouts de asyncio detalla timeout(), timeout_at(), reagendamiento y expiración. Usa deadlines absolutos para presupuestos compartidos, preserva cleanup y trata los timeouts como parte de una política de resiliencia.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Detailed view of a resting reticulated python showcasing its textured scales and intricate patterns.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    TaskGroup en Python: concurrencia estructurada

    Aprende asyncio.TaskGroup en Python para concurrencia estructurada, resultados, cancelación, ExceptionGroup, timeouts y grupos anidados.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A monochrome image of a lens on an open dictionary page, highlighting words.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    MappingProxyType: diccionario solo lectura

    Aprende MappingProxyType en Python para exponer diccionarios de solo lectura, crear vistas dinámicas y snapshots, y proteger invariantes sin copias.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Retro fuel pump with rusty metal and vintage design, featuring a nozzle and liter meter.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Literal en Python: restringe valores

    Aprende typing.Literal en Python para restringir valores, crear overloads, discriminar TypedDict, usar match/case y mejorar APIs tipadas.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    TypedDict en Python: diccionarios tipados

    Aprende TypedDict en Python para diccionarios tipados, claves opcionales, NotRequired, Required, payloads de APIs y variantes discriminadas.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Protocol en Python: tipado estructural

    Aprende Python Protocol para tipado estructural, contratos genéricos, callbacks, runtime_checkable, pruebas e inyección de dependencias.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    memoryview en Python: buffers sin copia

    Aprende memoryview en Python para buffers sin copia, slices, bytearray editable, cast, mmap, struct, sockets y control seguro del ciclo

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026