Trilha recomendada

Use este insight em tres movimentos

Leia o enquadramento, conecte-o a prova de implementacao e depois mantenha vivo o loop semanal de sinais para que esta pagina vire uma relacao mais longa com o site.

Esgotamento de Pool AsyncPG em APIs de Baixa Latência
Sistemas de Banco de Dados

Esgotamento de Pool AsyncPG em APIs de Baixa Latência

Resolva o esgotamento de pool AsyncPG em picos de eventos. Elimine erros 503 e regressões de p99 ajustando acquire timeouts e reciclagem de conexões.

2026-10-14 • 8 min

CompartilharLinkedInX

Esgotamento de Pool AsyncPG em APIs de Baixa Latência

O esgotamento de pool AsyncPG (AsyncPG connection pool starvation) paralisou nosso gateway de ingestão quando um pico de eventos bloqueou 400 corrotinas em sockets PostgreSQL sem resposta, elevando a latência p99 de 18 ms para 4.800 ms e descartando telemetria crítica. O alerta de plantão disparou às 02:14 UTC de um domingo. Consumidores de eventos travaram, erros HTTP 503 Service Unavailable ultrapassaram 22% do tráfego total e o consumo de memória dos processos backend do PostgreSQL forçou reinicializações via kernel out-of-memory. O reflexo operacional imediato de dobrar o tamanho do pool apenas agravou a disputa por locks e multiplicou a sobrecarga de agendamento de CPU no banco de dados.

APIs de alta taxa de transferência que conectam runtimes assíncronos em Python a bancos relacionais operam com margens estreitas. Quando milhares de corrotinas simultâneas disputam um pool mal dimensionado, pequenas variações na duração das queries causam colapsos em cadeia. Prevenir essa falha sistêmica exige dominar o ciclo de vida de aquisição de conexões, configurar limites estritos de timeout, reciclar conexões no asyncpg e acionar circuit breakers antes que esperas de conexão saturem seu gateway de aplicação.

Por Que Pools AsyncPG Esgotam Sob Picos de Eventos Concorrentes

O Asyncpg implementa um protocolo binário proprietário sobre TCP, contornando os bindings em C da libpq. Essa arquitetura oferece throughput elevado e baixo consumo de memória por instrução, frequentemente superando outros drivers Python por uma margem substancial. No entanto, sua implementação de pool assíncrono (asyncpg.pool.Pool) expõe o sistema a gargalos quando a taxa de ingestão supera a capacidade de processamento do banco.

Quando uma rota FastAPI executa async with pool.acquire():, a corrotina entra em uma fila de espera interna gerenciada por uma asyncio.Future. Se todas as conexões entre min_size e max_size estiverem ocupadas ou presas em transações sem commit, novas corrotinas acumulam-se nessa fila. Sem um timeout definido, pool.acquire() aguarda indefinidamente.

Sob alta concorrência, dois padrões provocam o esgotamento do pool:

  1. Criação Desenfreada de Tasks: O FastAPI gera uma corrotina independente para cada requisição HTTP recebida. Quando requisições chegam mais rápido do que o PostgreSQL conclui index scans ou locks de escrita, corrotinas se acumulam na memória. Cada corrotina em espera retém a conexão do cliente HTTP, consumindo descritores de arquivos enquanto aguarda um socket do banco de dados.
  2. Transações Longas e Vazamentos de Conexão: Uma exceção não tratada em blocos aninhados ou um encerramento incorreto de geradores assíncronos pode reter uma conexão fora do pool por tempo indeterminado. O pool considera a conexão ativa, reduzindo a capacidade útil até restarem zero conexões disponíveis.

Esse comportamento reflete os gargalos discutidos em mitigating Redis probabilistic early expiration, onde serviços desacoplados falham ao não proteger backends transacionais contra sobrecargas.

Diagnosticando Vazamentos Silenciosos em Lifespans do FastAPI

Equipes frequentemente descobrem o esgotamento do pool apenas após falhas em health checks. Métricas comuns de APM mascaram a causa raiz porque o uso de CPU no banco costuma permanecer baixo: o gargalo não é saturação computacional no servidor, mas corrotinas retidas esperando na fila de lock local do pool.

Para diagnosticar o estado do pool, inspecione as métricas internas da instância asyncpg.pool.Pool em vez de monitorar apenas a pg_stat_activity do PostgreSQL. As métricas determinantes incluem:

  • pool.get_size(): Total de conexões ativas alocadas pelo pool.
  • pool.get_idle_size(): Conexões prontas para execução imediata.
  • A diferença (pool.get_size() - pool.get_idle_size()): Conexões atualmente em uso.
  • Extensão da fila de espera: Número de futures bloqueadas em pool.acquire().

Quando get_idle_size() chega a zero e permanece estagnado enquanto a fila de espera cresce, o esgotamento do pool está degradando as requisições. Se get_size() for igual a max_size e a pg_stat_activity exibir conexões em estado idle in transaction ou idle, a aplicação está vazando conexões além dos limites de suas tasks.

Newsletter

Quer o proximo sinal antes de ele virar backlog?

Uma nota semanal curta: pressao de mercado, padrao de entrega e um link de prova que voce pode reutilizar.

Um email por semana. Sem spam. Apenas conteudo de alto sinal para tomadores de decisao.

Implementando Pooling Resiliente e Proteção com Timeout

Eliminar o esgotamento de conexões exige parâmetros explícitos para duração de aquisição, tempo de vida e timeouts de queries. Nunca execute pool.acquire() sem timeout defensivo, e estruture o pool para reciclar conexões antigas, evitando sockets TCP suspensos no event loop.

Abaixo apresentamos um padrão de lifespan para FastAPI com asyncpg, incorporando timeouts de aquisição, controle de execução e gerenciamento de recursos, conforme adotado em nosso projeto Streaming Radar API:

import asyncio
import logging
from contextlib import asynccontextmanager
from typing import AsyncGenerator
import asyncpg
from fastapi import FastAPI, HTTPException, status

logger = logging.getLogger("serving.database")

class DatabaseManager:
    def __init__(self):
        self._pool: asyncpg.Pool | None = None

    async def connect(
        self, 
        dsn: str, 
        min_connections: int = 10, 
        max_connections: int = 30,
        acquire_timeout_sec: float = 2.0
    ) -> None:
        self._acquire_timeout = acquire_timeout_sec
        self._pool = await asyncpg.create_pool(
            dsn=dsn,
            min_size=min_connections,
            max_size=max_connections,
            max_queries=50000,
            max_inactive_connection_lifetime=300.0,
            command_timeout=5.0,
            server_settings={
                "application_name": "fastapi_serving_node",
                "statement_timeout": "4000",
                "idle_in_transaction_session_timeout": "5000",
            }
        )
        logger.info("Pool Asyncpg inicializado (min=%d, max=%d)", min_connections, max_connections)

    async def disconnect(self) -> None:
        if self._pool:
            await self._pool.close()
            logger.info("Pool Asyncpg encerrado com sucesso")

    @asynccontextmanager
    async def acquire_context(self) -> AsyncGenerator[asyncpg.Connection, None]:
        if not self._pool:
            raise HTTPException(
                status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
                detail="Banco de dados nao inicializado"
            )
        try:
            conn = await self._pool.acquire(timeout=self._acquire_timeout)
        except asyncio.TimeoutError:
            logger.error("Timeout ao adquirir conexao apos %.2fs", self._acquire_timeout)
            raise HTTPException(
                status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
                detail="Pool de banco de dados esgotado"
            )
        try:
            yield conn
        finally:
            await self._pool.release(conn)

db = DatabaseManager()

@asynccontextmanager
async def lifespan(app: FastAPI):
    await db.connect(
        dsn="postgresql://writer:secure_pass@postgres-primary.internal:5432/events",
        min_connections=15,
        max_connections=35,
        acquire_timeout_sec=1.5
    )
    yield
    await db.disconnect()

app = FastAPI(lifespan=lifespan)

@app.get("/api/v1/metrics/live")
async def get_live_metrics(sensor_id: str):
    async with db.acquire_context() as conn:
        record = await conn.fetchrow(
            "SELECT sensor_id, current_val, updated_at FROM sensor_state WHERE sensor_id = $1",
            sensor_id
        )
        if not record:
            raise HTTPException(status_code=404, detail="Sensor nao encontrado")
        return dict(record)

Essa implementação constrói três barreiras fundamentais:

  • Timeout de Aquisição na Aplicação: O sistema responde com falha em 1,5 segundo em vez de reter requisições indefinidamente. Esse erro HTTP 503 imediato permite que balanceadores ou clientes repitam o envio de forma ordenada.
  • Statement Timeout: O parâmetro statement_timeout encerra comandos com duração superior a 4.000 milissegundos. Consultas não indexadas são interrompidas no motor do banco antes de travar conexões ativas.
  • Timeout de Transação Inativa: O servidor PostgreSQL descarta sessões mantidas em transação aberta por mais de 5.000 milissegundos, impedindo que conexões esquecidas segurem bloqueios nas tabelas.

Benchmarks em Produção: Dimensionamento Fixo vs Dinâmico Sob Carga

Para validar o dimensionamento ideal do pool, executamos testes de estresse simulando 2.500 requisições por segundo contra uma instância PostgreSQL 16 com 8 vCPUs. O fluxo conteve 80% de leituras pontuais indexadas e 20% de transações de escrita.

Avaliamos três estratégias distintas distribuídas entre quatro nós de execução:

  1. Sobrealocação Irrestrita: min_size=50, max_size=200 por nó (total potencial: 800 conexões).
  2. Pool Reduzido Subdimensionado: min_size=5, max_size=10 por nó (total: 40 conexões).
  3. Pool Dimensionado com Circuit Breakers: min_size=15, max_size=30 por nó (total: 120 conexões), com timeout de aquisição de 1,5 s e statement timeout de 4 s.

Durante o teste com Sobrealocação Irrestrita, o sistema entrou em degradação acelerada. Processos ativos no PostgreSQL ultrapassaram 650, elevando a sobrecarga de troca de contexto de CPU em 420%. A taxa efetiva caiu de 2.300 para 640 operações por segundo, e a latência p99 excedeu 6.200 milissegundos.

A estratégia de Pool Reduzido preservou a CPU do banco de dados estável, mas gerou enfileiramento crítico: mais de 31% das requisições falharam por indisponibilidade de conexões no client, violando nossos objetivos de serviço.

O Pool Dimensionado manteve throughput contínuo. As conexões ativas estabilizaram em 112, sem comprometer o planejador de queries do PostgreSQL. A latência média fixou-se em 14 milissegundos, o p99 permaneceu em 38 milissegundos, e os descartes controlados por timeout afetaram menos de 0,04% do tráfego nos picos mais severos.

Observabilidade e Telemetria para Prevenir Esgotamento de Conexões

Evitar indisponibilidades exige visibilidade contínua sobre a utilização dos recursos. Instrumentar o pool do asyncpg para expor métricas ao Prometheus permite identificar degradações antes que requisições sejam descartadas pelos nós da API.

Implemente um coletor em segundo plano com amostragem a cada cinco segundos integrado à sua infraestrutura de Data Observability Platform. Monitore quatro indicadores essenciais:

  • asyncpg_pool_connections_total{state="active"}: Conexões retidas executando operações.
  • asyncpg_pool_connections_total{state="idle"}: Conexões livres prontas para uso imediato.
  • asyncpg_pool_waiters_count: Corrotinas enfileiradas esperando liberação no pool.acquire().
  • asyncpg_pool_acquire_timeouts_total: Contador cumulativo de timeouts disparados na aquisição.

Defina regras de alerta quando asyncpg_pool_waiters_count permanecer superior a 10 por mais de 30 segundos, ou quando a taxa de asyncpg_pool_acquire_timeouts_total superar 0,1 eventos por segundo. Esses limiares antecipam a saturação do pool, permitindo escalar réplicas ou modular o tráfego antes do comprometimento dos gateways de serviço.

O alinhamento entre dimensionamento consciente e timeouts determinísticos garante que oscilações de tráfego falhem de forma controlada nas bordas, impedindo o colapso completo do banco de dados relacional.

CompartilharLinkedInX

Cluster do tema

Explore este tema entre prova e sinais vivos

Permaneça no mesmo tema mudando apenas o formato: saia do enquadramento estrategico e avance para prova de implementacao ou para um sinal fresco de mercado que mantenha a sessao em movimento.

Newsletter

Receba o proximo sinal estrategico antes do mercado assimilar.

Cada nota semanal conecta uma mudanca de mercado, um padrao de execucao e uma prova pratica que vale estudar.

Um email por semana. Sem spam. Apenas conteudo de alto sinal para tomadores de decisao.