asyncio.timeout no Python: controle prazos

Publicado em: 28/08/2026
Tempo de leitura: 5 minutos
Minimalist hourglass filled with sand symbolizing time and patience, against a soft background.

Operações assíncronas precisam de limites. Uma chamada de rede pode nunca responder, uma fila pode ficar vazia e um serviço lento pode consumir todo o tempo disponível de uma requisição. asyncio.timeout() cria um contexto com prazo: se o bloco não terminar a tempo, a tarefa é cancelada e o contexto transforma esse cancelamento em TimeoutError.

Neste guia, você aprenderá a usar timeouts relativos e absolutos, reagendar deadlines, verificar expiração, combinar com TaskGroup, comparar com asyncio.wait_for(), preservar cleanup e evitar erros comuns ao tratar cancelamento.

Primeiro timeout

import asyncio

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

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

asyncio.run(main())

Quando um segundo passa, o contexto cancela a tarefa atual. Ao sair do bloco, o cancelamento interno é convertido em TimeoutError, que deve ser capturado fora do async with.

Por que capturar fora do contexto?

A transformação de CancelledError em TimeoutError acontece na saída do gerenciador. Dentro do bloco, o mecanismo opera por cancelamento.

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

Esse padrão também deixa claro qual escopo possui o prazo.

Timeout não interrompe código bloqueante

Asyncio usa cancelamento cooperativo. O timeout só pode agir quando a coroutine entrega controle ao event loop. Código CPU-bound ou uma chamada bloqueante direta impede a reação.

async def incorreto():
    time.sleep(10)  # bloqueia o event loop

Para funções bloqueantes, use asyncio.to_thread(), um executor ou um processo.

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

O cancelamento deixa de aguardar a thread, mas não necessariamente interrompe a função que já está executando. Projete a operação bloqueante com seus próprios limites quando possível.

Timeout relativo e deadline absoluto

asyncio.timeout(segundos) define um limite relativo ao momento de entrada. asyncio.timeout_at(quando) recebe um instante do relógio monotônico do loop.

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

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

Deadlines absolutos são úteis quando várias camadas precisam compartilhar o mesmo orçamento sem reiniciar o cronômetro.

Propagando um orçamento de tempo

Imagine uma requisição com prazo total de cinco segundos. Cada função não deve ganhar cinco segundos novos; todas devem respeitar o mesmo deadline.

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

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

O tempo gasto na primeira etapa reduz automaticamente o disponível para a segunda.

Reagendando o timeout

O objeto retornado pelo contexto possui reschedule(). Isso é útil quando o deadline só é conhecido depois de ler metadados ou negociar com outro sistema.

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

    async with asyncio.timeout(None) as controle:
        limite = await obter_limite()
        controle.reschedule(loop.time() + limite)
        return await executar_trabalho()

None cria um contexto inicialmente sem prazo. O novo valor deve usar o relógio monotônico do loop.

Verificando se expirou

Após o contexto, expired() informa se o deadline foi atingido.

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

if controle is not None and controle.expired():
    print("o prazo expirou")

Normalmente, capturar TimeoutError já é suficiente. expired() ajuda em métricas e diagnósticos.

Timeouts aninhados

Contextos podem ser aninhados com segurança. O prazo mais curto dispara primeiro.

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

O timeout interno pode ser tratado localmente sem encerrar todo o fluxo, enquanto o timeout externo protege o orçamento total.

Combinando com TaskGroup

Um timeout ao redor de TaskGroup impõe prazo ao conjunto inteiro.

async def painel():
    try:
        async with asyncio.timeout(3.0):
            async with asyncio.TaskGroup() as grupo:
                usuario = grupo.create_task(obter_usuario())
                pedidos = grupo.create_task(obter_pedidos())
    except TimeoutError:
        return {"erro": "serviços lentos"}

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

Ao expirar, a tarefa contenedora é cancelada; o grupo cancela as tarefas filhas e aguarda cleanup.

Timeout individual em um grupo

Coloque o timeout dentro da tarefa filha quando cada operação possui seu próprio limite.

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

Se essa função deixar TimeoutError escapar, TaskGroup trata como falha e cancela as irmãs. Para um timeout esperado, capture internamente e retorne um resultado explícito.

Comparação com asyncio.wait_for

asyncio.wait_for(awaitable, timeout) envolve um awaitable específico. asyncio.timeout() envolve um bloco inteiro, permitindo múltiplos awaits e lógica intermediária.

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

async with asyncio.timeout(2.0):
    cabecalho = await ler_cabecalho()
    corpo = await ler_corpo(cabecalho)
    await validar(corpo)

O contexto costuma ser mais legível para fluxos compostos. wait_for() continua útil para uma única operação.

Cleanup e finally

A coroutine cancelada deve liberar recursos.

async def usar_conexao():
    conexao = await abrir_conexao()
    try:
        return await conexao.receber()
    finally:
        await conexao.fechar()

O timeout espera a propagação do cancelamento e a conclusão do cleanup. Um cleanup lento pode fazer a duração observada ultrapassar o prazo nominal. Mantenha encerramentos previsíveis e, quando necessário, dê limites próprios ao cleanup sem perder dados.

Não engolir CancelledError

Dentro da operação, o timeout é implementado com cancelamento. Capturar CancelledError e retornar normalmente impede que o contexto reconheça a expiração.

async def ruim():
    try:
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        return "ignorei"

O padrão correto é executar cleanup e relançar.

except asyncio.CancelledError:
    await limpar()
    raise

Shield e operações que não devem ser canceladas

asyncio.shield() pode proteger um awaitable interno do cancelamento da tarefa externa, mas exige cuidado.

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

await tarefa

O timeout deixa de aguardar, mas a tarefa protegida continua. Guarde uma referência forte e defina quem será responsável por esperá-la. Shield pode reintroduzir tarefas soltas se usado sem ownership claro.

Timeout não é retry

Um prazo encerra uma tentativa lenta; ele não decide se a operação deve ser repetida. Retentativas exigem política separada: quantidade máxima, backoff, jitter, idempotência e orçamento total.

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

O deadline absoluto impede que retries ultrapassem o orçamento da requisição.

Testes de timeout

Evite testes que dependem de pausas longas. Use tempos curtos com margem razoável, corrotinas controladas por asyncio.Event e fakes que não completam até receber sinal.

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

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

Não verifique milissegundos exatos em ambientes de CI. Teste o comportamento: erro, cleanup e cancelamento das tarefas relacionadas.

Métricas e observabilidade

Diferencie timeout de conexão, leitura, operação, fila e orçamento total. Registre a duração, o deadline restante, o serviço chamado e o identificador da requisição. Não transforme todo timeout em erro genérico sem contexto.

Uma métrica alta de timeout pode indicar saturação, dependência lenta, limite agressivo ou event loop bloqueado.

Erros comuns

  • Capturar TimeoutError dentro do contexto: a conversão ocorre ao sair.
  • Usar time.sleep em coroutine: o loop fica bloqueado.
  • Engolir CancelledError: o timeout pode não funcionar corretamente.
  • Reiniciar o orçamento em cada camada: prefira deadline absoluto.
  • Esperar que timeout mate uma thread: cancelamento assíncrono não interrompe código nativo arbitrário.
  • Usar shield sem ownership: tarefas podem continuar sem supervisão.

Exemplo completo: cliente com orçamento total

import asyncio

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

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

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

    return [tarefa.result() for tarefa in tarefas]

Todas as operações compartilham o mesmo deadline e pertencem ao mesmo grupo. O fluxo não cria um novo orçamento para cada serviço.

Conclusão

asyncio.timeout() transforma um prazo em um escopo claro. Ele usa cancelamento cooperativo, converte a expiração em TimeoutError e pode envolver várias operações relacionadas.

A documentação oficial de timeouts no asyncio detalha timeout(), timeout_at(), reagendamento e expiração. Use deadlines absolutos para orçamentos compartilhados, preserve cleanup e trate timeouts como parte de uma política maior de resiliência.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    A detailed view of computer programming code on a screen, showcasing software development.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    TaskGroup no Python: concorrência estruturada

    Aprenda asyncio.TaskGroup no Python para concorrência estruturada, resultados, cancelamento, ExceptionGroup, timeouts e tarefas aninhadas.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Black and white close-up of a dictionary page showing the definition of 'virus.'
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    MappingProxyType: dicionário só leitura

    Aprenda MappingProxyType no Python para expor dicionários somente leitura, criar visões dinâmicas, snapshots e proteger invariantes sem cópias.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026
    A close-up of a padlock securing a wire fence, symbolizing protection and safety.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Literal no Python: restrinja valores

    Aprenda typing.Literal no Python para restringir valores, criar overloads, discriminar TypedDict, usar match/case e melhorar APIs tipadas.

    Ler mais

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

    TypedDict no Python: dicionários tipados

    Aprenda TypedDict no Python para definir dicionários tipados, campos opcionais, NotRequired, Required, APIs e variantes com segurança estática.

    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 Avançado
    Foto de perfil de Leandro Hirt da Academify

    Protocol no Python: tipagem estrutural

    Aprenda typing.Protocol no Python para tipagem estrutural, contratos genéricos, callbacks, runtime_checkable, testes e baixo acoplamento.

    Ler mais

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

    memoryview no Python: buffers sem cópia

    Aprenda memoryview no Python para acessar buffers sem cópia, criar slices, editar bytearray, usar cast, mmap, struct e sockets com

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026