PostgreSQL Connection Pooling no Django: Como Usar Pools Nativos do Django 5.1 e PgBouncer para Alta Performance em Produção

Published on: 2026-01-25
Post image
pt postgresql-connection-pooling django-postgresql-performance django-connection-pooling django-51-connection-pool django-native-pooling psycopg3-connection-pool pgbouncer-django pgbouncer-postgresql django-database-performance django-production

Em aplicações Django com PostgreSQL, praticamente toda requisição depende de uma conexão com o banco de dados. Abrir uma conexão do zero custa tempo e recursos, porque envolve etapas como negociação de rede, autenticação e alocação de memória no servidor de banco. Em volume alto de tráfego, esse custo deixa de ser detalhe e vira um dos maiores gargalos do sistema.

Connection pooling (pool de conexões) resolve esse problema ao manter conexões prontas para reutilização, evitando criar e destruir conexões a cada ciclo de requisição. Há duas abordagens principais: o pool nativo do Django (a partir do Django 5.1, com psycopg3) e o uso de um pool externo como o PgBouncer, que centraliza o controle de conexões para vários processos e serviços.

Por que abrir conexão com PostgreSQL custa caro

Uma conexão não é apenas “abrir um socket” e começar a consultar. Em geral, existem etapas de comunicação inicial, validação de credenciais e preparação de estruturas internas no cliente e no servidor. Em redes com latência, o custo sobe ainda mais e varia bastante conforme criptografia, firewall e distância. Em cenários concorridos, esse tempo se repete muitas vezes por segundo e vira consumo de CPU e tempo de resposta.

Um jeito simples de enxergar o custo é medir quanto tempo uma conexão leva para ficar pronta. O exemplo a seguir mede a duração da chamada de conexão usando psycopg2, apenas para ilustrar o custo bruto do “conectar”. Mesmo em ambiente local, valores de alguns milissegundos são comuns, e em rede podem chegar a centenas.

import time
import psycopg2

inicio = time.perf_counter()
conexao = psycopg2.connect(
    host="localhost",
    dbname="minha_app",
    user="postgres",
    password="senha",
)
decorrido = time.perf_counter() - inicio
print(f"Tempo de conexão: {decorrido * 1000:.2f}ms")

conexao.close()

Limites de conexão e impacto de memória no PostgreSQL

O PostgreSQL possui um limite máximo de conexões simultâneas definido por max_connections, que frequentemente começa em torno de 100 em instalações padrão. Cada conexão consome memória e estruturas internas no servidor, o que torna perigoso “resolver” tudo apenas aumentando o limite. Além disso, múltiplos processos de aplicação multiplicam a quantidade de conexões abertas, mesmo que cada processo use poucas. Em pouco tempo, o banco pode ficar saturado de conexões ociosas e ainda assim negar novas entradas.

Em servidores web com múltiplos workers, a conta cresce rápido. Um conjunto de processos Gunicorn, mais workers de tarefas assíncronas como Celery, mais jobs de manutenção, já somam dezenas de conexões fixas. A partir daí, qualquer pico de criação e destruição de conexões aumenta a instabilidade e piora a latência. Por isso, o pool é mais do que performance: é também previsibilidade e controle de capacidade.

O que é connection pooling e como ele muda o “antes e depois”

Sem pool, o padrão mais simples cria uma conexão por requisição (ou abre e fecha com frequência), repetindo o custo de handshake e autenticação. Com pool, o sistema mantém um conjunto de conexões abertas e reutilizáveis, entregando uma conexão já pronta quando surge demanda. Quando a operação termina, a conexão volta ao pool e fica disponível. O resultado típico é redução de latência e aumento de estabilidade sob carga.

Existem variações importantes do que “reutilizar” significa, especialmente quando um pool é externo. Em alguns modelos, uma conexão fica “presa” a um cliente até ele desconectar; em outros, a conexão volta ao pool ao final de uma transação. Essa diferença afeta compatibilidade com recursos do PostgreSQL que dependem de estado de sessão. A escolha correta depende do volume, do estilo de uso do banco e dos recursos utilizados.

Pool nativo do Django 5.1 para PostgreSQL

O Django 5.1 introduziu pool nativo para PostgreSQL, utilizando o pool do psycopg3. O objetivo é reduzir drasticamente o custo de conexões em aplicações comuns, sem exigir uma camada de infraestrutura adicional. Esse pool é gerenciado no nível do processo, ou seja, cada worker do servidor de aplicação mantém seu próprio conjunto de conexões. Em muitos ambientes, isso resolve a maior parte do problema com configuração mínima.

Esse recurso depende do pacote correto, pois ele utiliza psycopg (psycopg3) e não psycopg2. A instalação costuma incluir o extra de pool para habilitar o recurso. A partir disso, o Django consegue administrar conexões reaproveitáveis e reduzir a “churn” (abertura/fechamento) em momentos de pico.

Pré-requisitos: psycopg3 com suporte a pool

O pool nativo exige psycopg3, distribuído no pacote psycopg. Em produção, é comum usar a variante binária para simplificar dependências, e habilitar o extra de pool. A instalação é simples, mas precisa ser consistente no ambiente onde a aplicação roda. Caso exista dependência antiga de psycopg2, a migração deve ser planejada para evitar incompatibilidades.

A seguir está o comando de instalação típico. Ele inclui os extras binary e pool, que cobrem tanto a distribuição binária quanto os componentes do pool. O resultado é um ambiente pronto para o Django ativar pooling pela configuração.

pip install "psycopg[binary,pool]"

Configuração básica do pool nativo no Django

A configuração mais simples habilita o pool com uma chave booleana. Nesse formato, o Django cria um pool com padrões internos do psycopg3 e passa a reutilizar conexões dentro do processo. Esse modo é útil quando o objetivo é reduzir custo de conexão sem ajustes finos. Mesmo assim, o efeito em carga costuma ser bastante perceptível.

O exemplo abaixo mostra uma configuração típica de produção com variáveis de ambiente e a opção de pool habilitada. A chave OPTIONS é onde o Django recebe a configuração específica do driver. A partir desse ponto, cada processo Django passa a manter conexões prontas para uso.

# config/settings/production.py
from decouple import config

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": config("DB_NAME"),
        "USER": config("DB_USER"),
        "PASSWORD": config("DB_PASSWORD"),
        "HOST": config("DB_HOST"),
        "PORT": config("DB_PORT", default="5432"),
        "OPTIONS": {
            "pool": True,
        },
    }
}

Configuração avançada do pool nativo: tamanhos e tempos

Em ambientes com concorrência real, ajustes de pool controlam a previsibilidade: quantas conexões ficam abertas, quanto tempo esperar por uma conexão, quando reciclar conexões antigas e quando fechar conexões ociosas. Esses parâmetros ajudam a equilibrar latência e consumo de recursos no PostgreSQL. Valores muito baixos podem aumentar espera; valores muito altos podem estourar max_connections. O ideal é configurar com base no número de processos e no limite do banco.

O exemplo a seguir define parâmetros comuns: min_size e max_size determinam o tamanho do pool por processo; timeout controla quanto tempo aguardar por uma conexão livre; max_idle e max_lifetime reciclam conexões para evitar acúmulo de conexões envelhecidas. reconnect_timeout ajuda a tolerar instabilidades breves ao tentar restabelecer conexões.

# config/settings/production.py
from decouple import config

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": config("DB_NAME"),
        "USER": config("DB_USER"),
        "PASSWORD": config("DB_PASSWORD"),
        "HOST": config("DB_HOST"),
        "PORT": config("DB_PORT", default="5432"),
        "OPTIONS": {
            "pool": {
                "min_size": 2,
                "max_size": 10,
                "timeout": 10,
                "max_idle": 300,
                "max_lifetime": 3600,
                "reconnect_timeout": 5,
            },
        },
    }
}

Como calcular o tamanho do pool no Django sem estourar o PostgreSQL

Como cada processo possui seu próprio pool, o total de conexões potencial é o número de processos multiplicado por max_size. Isso inclui workers do servidor web, workers de tarefas e qualquer outro processo que use a mesma configuração. Um erro comum é configurar um pool “bonito” em cada processo e, na soma, ultrapassar max_connections. Uma margem de segurança é importante para conexões administrativas e operações ocasionais.

A lógica a seguir mostra um cálculo simples, considerando uma reserva para o sistema e dividindo o restante pelo total de processos. Esse cálculo não substitui observação real, mas oferece um ponto de partida consistente. Também é importante lembrar que picos acontecem, e o pool deve ter limite que proteja o banco. Limitar o pool pode gerar espera na aplicação, mas evita colapso do PostgreSQL.

# Exemplo conceitual de cálculo (não é código de runtime)
# max_size = (max_connections - reserva) / total_de_processos

max_connections = 100
reserva = 20
total_de_processos = 8

max_size = (max_connections - reserva) // total_de_processos
print(max_size)  # 10

Quando o pool nativo do Django costuma ser suficiente

O pool nativo tende a funcionar muito bem quando o número de processos é moderado e a arquitetura é simples. Como o pool é por processo, ambientes com poucos servidores e poucos workers conseguem reduzir bastante a sobrecarga sem adicionar componentes. Também funciona bem quando o acesso ao banco é praticamente todo via Django, reduzindo a chance de concorrência inesperada com outros serviços. Além disso, é uma abordagem direta para reduzir latência de conexão em aplicações web tradicionais.

Em termos de compatibilidade, ele se encaixa naturalmente em um sistema que já utiliza psycopg3. Quando o objetivo é reduzir custo de conexão e manter simplicidade operacional, esse caminho costuma ser preferível. Em escala maior, o problema deixa de ser “reutilizar” e passa a ser “centralizar e limitar globalmente” o número de conexões do cluster. Nesse ponto, um pool externo se torna mais interessante.

CONN_MAX_AGE: conexões persistentes como abordagem anterior

Antes do pool nativo, uma técnica comum era configurar CONN_MAX_AGE, que mantém uma conexão viva por um período e a reutiliza no mesmo processo. Isso reduz o abre-fecha a cada requisição, mas não é um pool de verdade, porque normalmente existe uma conexão “ativa” por thread/processo e a reutilização é mais simples. O benefício aparece, mas é limitado em comparação com pooling real sob concorrência. Também é fácil cair em armadilhas quando conexões ficam velhas após reinícios do PostgreSQL.

O exemplo abaixo mantém a conexão aberta por 60 segundos, evitando reconectar a cada requisição. O valor None pode manter indefinidamente, mas aumenta o risco de conexões quebradas permanecerem até o sistema detectar falha. O valor 0 equivale a fechar ao final da requisição, o que é o comportamento mais caro em termos de overhead.

# config/settings/production.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "minha_app",
        "USER": "meu_usuario",
        "PASSWORD": "minha_senha",
        "HOST": "db",
        "PORT": "5432",
        "CONN_MAX_AGE": 60,
    }
}

PgBouncer: pooling externo e centralizado

O PgBouncer é um serviço que fica entre a aplicação e o PostgreSQL, recebendo muitas conexões de clientes e mantendo poucas conexões reais com o banco. Ele funciona como um “intermediário de conexões”, permitindo que vários processos e até várias aplicações compartilhem um conjunto controlado de conexões. Isso reduz o número total de conexões no PostgreSQL e melhora a estabilidade em arquiteturas distribuídas. Esse modelo costuma ser essencial quando há muitos servidores de aplicação ou ambientes elásticos.

Outra vantagem é a independência de linguagem: qualquer cliente PostgreSQL pode usar PgBouncer sem mudanças profundas. O modo de pooling mais importante para aplicações web é o transaction pooling, que devolve a conexão ao pool ao final de cada transação. Isso aumenta muito a eficiência, mas exige cuidados com recursos que dependem de estado de sessão. Esses cuidados precisam ser compreendidos para evitar bugs difíceis de rastrear.

Instalação do PgBouncer em sistemas comuns

A instalação do PgBouncer depende do sistema operacional e do gerenciador de pacotes. Em servidores Linux baseados em Debian/Ubuntu, normalmente é um pacote oficial. Em macOS, pode ser instalado via Homebrew para desenvolvimento local. Em produção, o mais importante é ter um serviço gerenciado, com arquivo de configuração estável e logs habilitados.

Os comandos abaixo representam os métodos mais comuns de instalação. Em ambientes com containers, o padrão é usar uma imagem pronta, mas a lógica de configuração continua a mesma. O ponto central é que PgBouncer deve conseguir se conectar ao PostgreSQL e aceitar conexões dos clientes no endereço/porta definidos.

# Ubuntu/Debian
sudo apt install pgbouncer

# macOS
brew install pgbouncer

Configuração básica do PgBouncer: pgbouncer.ini

A configuração do PgBouncer define dois aspectos principais: como mapear “bancos lógicos” para o PostgreSQL real e como gerenciar o pool. A seção [databases] cria um apelido para um banco, que pode apontar para host, porta e dbname reais. A seção [pgbouncer] define onde o PgBouncer escuta, como autentica, qual modo de pool usa e quais limites aplicar. Um bom ponto de partida é limitar clientes e controlar o número de conexões reais ao PostgreSQL.

O arquivo abaixo ilustra uma configuração robusta para ambiente local, com pool_mode = transaction, limites e timeouts típicos. Os parâmetros default_pool_size, min_pool_size e reserve_pool_size regulam quantas conexões ao PostgreSQL são mantidas por banco. O limite max_client_conn define quantos clientes podem conectar no PgBouncer, independentemente do número de conexões reais ao banco.

; /etc/pgbouncer/pgbouncer.ini

[databases]
minha_app = host=127.0.0.1 port=5432 dbname=minha_app_producao

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432

auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt

pool_mode = transaction

max_client_conn = 1000
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3

server_idle_timeout = 600
client_idle_timeout = 0
server_connect_timeout = 15
server_login_retry = 15

log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
stats_period = 60

admin_users = pgbouncer_admin

Autenticação no PgBouncer: userlist.txt e hash MD5

O PgBouncer autentica clientes usando um arquivo de usuários quando configurado com auth_file. Esse arquivo associa usuário a senha, muitas vezes no formato MD5 com prefixo “md5”, que é o padrão clássico do ecossistema PostgreSQL. O hash MD5 usado nesse caso é calculado a partir da concatenação da senha com o nome de usuário, e então aplicado MD5. Esse detalhe é importante porque não é o mesmo MD5 “apenas da senha”.

O exemplo abaixo mostra o formato do arquivo e um comando para gerar o hash. Em ambientes reais, a gestão dessas credenciais deve ser consistente com a forma como o PostgreSQL autentica e com as políticas internas de segurança. Quando a configuração utiliza senha em texto puro, o risco aumenta, especialmente se o arquivo tiver permissões inadequadas.

; /etc/pgbouncer/userlist.txt
"minha_app_user" "md5_hash_aqui"
"pgbouncer_admin" "md5_hash_admin_aqui"
# Gera o MD5 no formato esperado: md5(MD5(senha + usuario))
# Exemplo: senha="senha", usuario="minha_app_user"
echo -n "senhaminha_app_user" | md5sum

# Depois prefixar com "md5", ficando: md5<hash>

Modos de pool no PgBouncer e diferenças práticas

O PgBouncer possui três modos principais de pooling, e a escolha afeta compatibilidade e eficiência. No session pooling, uma conexão ao PostgreSQL fica reservada para o cliente até ele desconectar, sendo o modo mais compatível e menos eficiente. No transaction pooling, a conexão é atribuída no início da transação e devolvida ao final, o que combina muito bem com workloads web de transações curtas. No statement pooling, a conexão é devolvida após cada comando, sendo agressivo e potencialmente incompatível com transações multi-comando.

O resumo abaixo mostra os modos e a implicação principal de cada um. Em aplicações Django, o modo mais comum para escala é transaction, desde que alguns recursos dependentes de sessão sejam evitados ou ajustados. Quando há necessidade forte de estado de sessão no PostgreSQL, session pode ser mais apropriado, embora menos eficiente. O modo statement raramente é a melhor escolha para aplicações com transações reais.

  • session: conexão dedicada ao cliente até desconectar, máxima compatibilidade e menor ganho.

  • transaction: conexão dedicada apenas durante a transação, ótimo para web e alta concorrência.

  • statement: conexão por comando, maior eficiência teórica e maior chance de quebra de comportamento.

Configurando Django para usar PgBouncer corretamente

Quando PgBouncer é usado, o Django deve apontar para o host e porta do PgBouncer, não para o PostgreSQL diretamente. Também é comum desabilitar mecanismos que conflitam com pooling externo, como deixar CONN_MAX_AGE em 0 para não “persistir” conexões do lado do Django, já que o intermediário gerencia isso. Além disso, quando o modo é transaction, certas funcionalidades do Django/PostgreSQL precisam de ajustes, principalmente cursores do lado do servidor. Uma configuração bem feita evita bugs e melhora a previsibilidade.

O exemplo abaixo mostra um settings típico com PgBouncer em 127.0.0.1:6432, CONN_MAX_AGE desativado e o pool nativo do Django desligado para evitar sobreposição. A opção DISABLE_SERVER_SIDE_CURSORS é crucial em transaction pooling, pois esses cursores dependem de uma sessão contínua. Sem isso, iterações grandes podem falhar ou se comportar de forma inesperada.

# config/settings/production.py
from decouple import config

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "minha_app",  # deve casar com a seção [databases] do PgBouncer
        "USER": config("DB_USER"),
        "PASSWORD": config("DB_PASSWORD"),
        "HOST": "127.0.0.1",
        "PORT": "6432",
        "CONN_MAX_AGE": 0,
        "OPTIONS": {
            "pool": False,
        },
        "DISABLE_SERVER_SIDE_CURSORS": True,
    }
}

Recursos que podem quebrar com transaction pooling

O modo transaction devolve a conexão ao pool ao final da transação, então qualquer recurso que dependa de “permanecer na mesma sessão” pode falhar. Isso inclui LISTEN/NOTIFY para eventos, temporary tables (tabelas temporárias) que vivem na sessão, e certos usos de advisory locks (travas consultivas) que assumem continuidade. Também existem interações delicadas com prepared statements (declarações preparadas) e comandos SET que alteram parâmetros por sessão. A mitigação geralmente envolve evitar esses padrões, mudar o modo do pool ou isolar conexões diretas para casos especiais.

A lista abaixo mostra exemplos típicos do que pode dar problema e o motivo resumido. O ponto central é que, após o fim da transação, a próxima transação pode cair em outra conexão do pool. Se o código presume que “é a mesma conexão”, o comportamento não é garantido. Esse é o preço do ganho alto de eficiência em workloads web.

  • LISTEN/NOTIFY: exige conexão persistente para receber notificações.

  • temporary tables: tabelas temporárias são escopo de sessão, não de transação.

  • advisory locks: podem não persistir de forma útil entre transações com conexões diferentes.

  • SET por sessão: mudanças podem “vazar” ou ser resetadas conforme a política do pool.

Monitoramento do PgBouncer via console administrativo

O PgBouncer fornece um console administrativo acessível via psql conectando no banco virtual “pgbouncer”. Esse console expõe estatísticas de pools, clientes, servidores e configuração ativa. Com isso, é possível entender se há fila esperando conexão, quantas conexões reais estão abertas no PostgreSQL e quantos clientes estão conectados no intermediário. Esse monitoramento é uma parte essencial para evitar surpresas em horários de pico.

Os comandos abaixo cobrem as consultas mais úteis para acompanhar a saúde do pool. SHOW POOLS revela quantos clientes estão esperando e quantas conexões estão ativas por banco. SHOW SERVERS mostra conexões reais ao PostgreSQL. SHOW CLIENTS mostra conexões vindas da aplicação, ajudando a visualizar explosões de concorrência.

-- Conectar no console:
-- psql -p 6432 -U pgbouncer_admin pgbouncer

SHOW POOLS;
SHOW STATS;
SHOW SERVERS;
SHOW CLIENTS;
SHOW CONFIG;

Monitoramento no PostgreSQL: conexões e estados

Do lado do PostgreSQL, a visão clássica é a tabela de estatísticas pg_stat_activity, que mostra conexões, estados e consultas. Isso ajuda a distinguir se o problema está em “muitas conexões” ou em “consultas lentas segurando conexões”. O estado idle indica conexão ociosa; active indica execução; e há variações intermediárias dependendo de versão e atividade. Consultas longas podem segurar conexões por muito tempo, reduzindo a eficácia de qualquer pool.

As consultas abaixo mostram contagem total, contagem por estado, por aplicação e consultas longas. Em ambientes com PgBouncer, a contagem de conexões no PostgreSQL tende a ficar menor e mais estável, enquanto o PgBouncer absorve o volume de clientes. Mesmo assim, uma contagem pequena não significa saúde se existirem consultas longas dominando o tempo de CPU ou bloqueios.

SELECT count(*) FROM pg_stat_activity;

SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;

SELECT application_name, count(*)
FROM pg_stat_activity
GROUP BY application_name;

SELECT pid,
       now() - pg_stat_activity.query_start AS duracao,
       query,
       state
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > interval '5 minutes';

Monitoramento no Django: métricas simples de conexão no runtime

Além de ferramentas de desenvolvimento, é possível criar um monitoramento simples no Django para registrar quando uma conexão foi aberta durante uma requisição. Isso ajuda a identificar churn de conexões ou padrões inesperados, especialmente quando pooling está mal configurado. Um middleware pode comparar o estado anterior e posterior de conexão e registrar eventos relevantes. Esse tipo de instrumentação é útil para investigar “por que está reconectando tanto” mesmo com pooling habilitado.

O exemplo abaixo registra quando uma conexão nova foi estabelecida ao longo do processamento. Um detalhe importante é que o logger precisa existir e estar configurado no projeto para que o registro vá para um destino útil. Esse middleware não mede o tempo do banco em si, mas ajuda a perceber quando conexões estão sendo criadas mais do que o esperado. Em produção, logs desse tipo devem ser moderados para não gerar excesso de volume.

import time
import logging
from django.db import connection

logger = logging.getLogger(__name__)

class MonitoramentoConexaoDBMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        estava_conectado = connection.connection is not None

        inicio = time.perf_counter()
        response = self.get_response(request)
        decorrido = time.perf_counter() - inicio

        esta_conectado = connection.connection is not None

        if not estava_conectado and esta_conectado:
            logger.info("Nova conexão com o banco criada em %s (%.2fms)", request.path, decorrido * 1000)

        return response

Erros comuns e diagnóstico: “too many connections”

O erro FATAL: too many connections acontece quando o PostgreSQL atinge max_connections e passa a negar novas conexões. Isso pode ocorrer por pool mal dimensionado, número excessivo de processos ou por conexões “presas” por consultas longas. Em ambientes sem PgBouncer, o crescimento de processos pode gerar explosão de conexões rapidamente. Em ambientes com PgBouncer, o erro pode indicar que o PgBouncer está criando conexões demais ao PostgreSQL ou que o limite do PostgreSQL está muito baixo para o contexto.

As correções mais comuns envolvem reduzir max_size do pool por processo, reduzir o número de workers do servidor web, ou introduzir PgBouncer para centralizar e limitar. Aumentar max_connections pode ajudar, mas aumenta consumo de memória e não resolve a causa estrutural se o padrão de uso continuar ruim. O ideal é forçar um teto de conexões e estabilizar a arquitetura, para que o banco nunca receba um volume imprevisível de conexões simultâneas.

Erros comuns e diagnóstico: timeouts e conexões derrubadas

Time out de conexão costuma indicar problema de rede, firewall, DNS ou banco indisponível. Já o erro de “server closed the connection unexpectedly” pode ocorrer após reinício do PostgreSQL, interrupção de rede ou quando um intermediário encerra conexões por timeout. Em pooling, isso é mais visível porque conexões ficam vivas por mais tempo e podem “morrer” no meio do caminho. Por isso, políticas de reciclagem e checagens de saúde ajudam a reduzir impacto.

No pool nativo, configurar max_lifetime recicla conexões para evitar manter conexões antigas por tempo demais. No Django, CONN_HEALTH_CHECKS ajuda a verificar se uma conexão persistente ainda está válida antes de usá-la. Essas medidas reduzem a chance de uma requisição cair exatamente em uma conexão quebrada e falhar. Em ambientes com PgBouncer, timeouts como server_idle_timeout e server_connect_timeout também são parte do equilíbrio.

# config/settings/production.py
from decouple import config

CONN_HEALTH_CHECKS = True

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": config("DB_NAME"),
        "USER": config("DB_USER"),
        "PASSWORD": config("DB_PASSWORD"),
        "HOST": config("DB_HOST"),
        "PORT": config("DB_PORT", default="5432"),
        "OPTIONS": {
            "pool": {
                "min_size": 2,
                "max_size": 10,
                "timeout": 30,
                "max_idle": 300,
                "max_lifetime": 1800,
            },
            "connect_timeout": 10,
        },
    }
}

Recuperação após reinício do PostgreSQL: fechando conexões antigas

Após um reinício do PostgreSQL, conexões existentes deixam de ser válidas e tentativas de uso podem falhar. Pools bem configurados normalmente se recuperam, mas há cenários em que conexões ficam em estado ruim e precisam ser fechadas para forçar a recriação. Em aplicações long-lived, fechar conexões explicitamente pode ser útil em rotinas de manutenção ou em ações administrativas. No Django, o objeto connections permite iterar e fechar conexões conhecidas.

O exemplo abaixo fecha todas as conexões do Django no processo atual, forçando novas conexões quando necessário. Em produção, isso deve ser usado com cuidado, pois pode causar pico momentâneo de reconexão se executado em muitos processos ao mesmo tempo. Ainda assim, é uma ferramenta simples para restaurar o estado quando há suspeita de conexões “stale” (obsoletas). Para operações controladas, um comando de management pode padronizar essa ação.

from django.db import connections

for conexao in connections.all():
    conexao.close()

Comando de management para resetar conexões de forma padronizada

Um management command é uma forma oficial do Django empacotar rotinas administrativas. Esse tipo de comando pode ser usado para ações operacionais controladas, com saída padronizada e integração com o ecossistema do projeto. No caso de reset de conexões, o comando percorre todas as conexões registradas e as fecha. Isso não “desliga o banco”, apenas encerra conexões do processo que rodar o comando.

O exemplo abaixo mostra um comando completo, com nome e ajuda, adequado para inclusão em um app Django. Ele usa a base BaseCommand e imprime uma mensagem de sucesso ao final. Esse padrão é útil quando existe necessidade de uniformizar ações operacionais sem depender de shell interativo. O comando opera localmente no processo em que é executado.

from django.core.management.base import BaseCommand
from django.db import connections

class Command(BaseCommand):
    help = "Reseta todas as conexões com o banco de dados do processo atual"

    def handle(self, *args, **options):
        for conexao in connections.all():
            conexao.close()
        self.stdout.write(self.style.SUCCESS("Conexões resetadas com sucesso"))

Ajustes no PostgreSQL que interagem com pooling

Pooling não elimina a necessidade de dimensionar o PostgreSQL, mas muda o foco para manter um número menor e mais estável de conexões. Ajustar max_connections pode ser necessário, mas aumentar indiscriminadamente pode aumentar consumo de memória e reduzir eficiência do cache interno. Parâmetros como shared_buffers e work_mem também influenciam desempenho, especialmente sob concorrência, porque determinam quanto o banco pode manter em memória e quanto cada operação pode usar para ordenações e hash joins. Alterações devem ser coerentes com a memória disponível e com o perfil de queries.

Os comandos abaixo exemplificam mudanças de configuração via ALTER SYSTEM, seguidas de recarregamento. Em alguns parâmetros, pode ser necessário reiniciar o PostgreSQL em vez de apenas recarregar, dependendo da versão e do parâmetro. Esses valores são ilustrativos e devem refletir capacidade de máquina e padrão de carga. O ponto central é manter o banco dentro de limites seguros e previsíveis enquanto o pool controla o acesso concorrente.

ALTER SYSTEM SET max_connections = 200;
ALTER SYSTEM SET shared_buffers = '256MB';
ALTER SYSTEM SET work_mem = '16MB';

SELECT pg_reload_conf();

Escolha prática: pool nativo do Django, PgBouncer ou abordagem híbrida

O pool nativo do Django é um ótimo padrão quando a arquitetura é simples, há poucos servidores e o objetivo é reduzir overhead sem introduzir um novo componente. O PgBouncer se destaca quando existe necessidade de centralizar e controlar conexões em um ambiente com muitos processos, múltiplos serviços ou alto crescimento elástico. A escolha também pode ser influenciada por compatibilidade: PgBouncer pode ajudar em ambientes com clientes legados e mistura de tecnologias. Em sistemas muito grandes, a consistência do número de conexões no PostgreSQL costuma ser um benefício decisivo.

Há cenários em que uma abordagem híbrida é útil: um pool pequeno no Django reduz churn entre o Django e o PgBouncer, enquanto o PgBouncer limita e estabiliza as conexões reais com o PostgreSQL. Esse desenho pode reduzir overhead em ambas as camadas, mas exige disciplina para não multiplicar conexões desnecessariamente. Em geral, o objetivo é manter baixos e previsíveis os números em cada fronteira. O exemplo abaixo mostra um pool pequeno por processo apontando para o PgBouncer.

# config/settings/production.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "minha_app",
        "USER": "meu_usuario",
        "PASSWORD": "minha_senha",
        "HOST": "pgbouncer.interno",
        "PORT": "6432",
        "OPTIONS": {
            "pool": {
                "min_size": 1,
                "max_size": 4,
                "timeout": 10,
                "max_idle": 120,
                "max_lifetime": 1800,
            },
        },
        "DISABLE_SERVER_SIDE_CURSORS": True,
    }
}

Encerramento: como o pooling organiza o ponto mais crítico do stack

Pool de conexões transforma o custo de “conectar” em um evento raro e controlado, em vez de um custo repetido a cada requisição. No Django moderno, o pool nativo oferece uma solução direta para muitas aplicações, com boa redução de latência e menos churn. Em escala maior, o PgBouncer se torna uma peça estratégica por centralizar conexões e permitir transaction pooling, mantendo o PostgreSQL protegido de explosões de concorrência. O resultado final é um sistema mais estável, com desempenho mais previsível e melhor uso de recursos.

O ponto decisivo é tratar conexões como capacidade finita e valiosa, dimensionando limites com base no número de processos e no max_connections do PostgreSQL. Uma configuração coerente evita erros como exaustão de conexões, timeouts intermitentes e quedas após reinícios do banco. Com monitoramento básico no PgBouncer e no PostgreSQL, o comportamento do pool se torna observável e ajustável. Dessa forma, o stack fica menos sujeito a gargalos invisíveis e mais resistente a variações de carga.