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

    Código en una pantalla que representa t-strings en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    T-strings en Python 3.14: templates estructurados

    Aprende t-strings en Python 3.14, explora Template e Interpolation y crea renderizadores estructurados, extensibles y seguros.

    Ler mais

    Tempo de leitura: 6 minutos
    16/10/2026
    CPU y módulos de memoria que representan shared_memory track en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    shared_memory track: controla el resource tracker

    Aprende shared_memory track en Python para controlar el resource tracker, la propiedad y la limpieza entre procesos.

    Ler mais

    Tempo de leitura: 5 minutos
    16/10/2026
    Unidad de almacenamiento de datos que representa compression.zstd en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    compression.zstd: streaming y compresión segura

    Aprende compression.zstd en Python para comprimir archivos y datos con velocidad, streaming, diccionarios y límites seguros.

    Ler mais

    Tempo de leitura: 7 minutos
    15/10/2026
    Código en pantalla para diagnosticar procesos con sys.remote_exec en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    sys.remote_exec: diagnostica procesos Python

    Aprende sys.remote_exec en Python para diagnosticar procesos activos con scripts controlados, permisos seguros y buenas prácticas.

    Ler mais

    Tempo de leitura: 7 minutos
    15/10/2026
    Código Python y configuración de variables de entorno con os.reload_environ
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    os.reload_environ: threads y variables de entorno

    Aprende os.reload_environ en Python para actualizar variables de entorno externas con validación, compatibilidad y precauciones de concurrencia.

    Ler mais

    Tempo de leitura: 5 minutos
    14/10/2026
    Código Python que representa contextvars.Token y aislamiento de contexto asíncrono
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    contextvars.Token: restaura contexto async

    Aprende contextvars.Token en Python para valores temporales, restauración automática y aislamiento seguro entre tareas asíncronas.

    Ler mais

    Tempo de leitura: 7 minutos
    14/10/2026