Programming On Mars Logo
  • Início
  • Artigos
  • Laboratórios
Programming On Mars Logo
  • Início
  • Artigos
  • Laboratórios

  • Andre Lucas
  • Sat Feb 15 2026

HikariCP Connection Pooling com PostgreSQL - O Guia Prático

Cada Milissegundo Conta

Se você trabalha com aplicações Spring Boot em produção, já deve ter enfrentado aquele momento em que o sistema começa a engasgar sob carga. Latência subindo, threads bloqueadas, e quando você investiga, o gargalo está lá: conexões com o banco de dados.

Nos últimos meses, trabalhando com sistemas event-driven que processam milhares de transações por segundo, aprendi que a forma como você gerencia conexões com o PostgreSQL pode ser a diferença entre uma aplicação que responde em 11ms e uma que trava em produção.

E o HikariCP está no centro dessa conversa.

O Problema: Criar Conexões é Caro

Antes de falar de pool, vale entender o custo real de abrir uma conexão com o banco.

// Sem connection pool
Connection conn = DriverManager.getConnection("jdbc:postgresql://...", "admin", "admin");
// Criar essa conexão demora ~100-200ms
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM transaction_events");
ResultSet rs = stmt.executeQuery();
// ... processar dados
conn.close(); // Fecha e destrói a conexão

Cada vez que a aplicação precisa falar com o PostgreSQL sem pool, acontece:

  • Handshake TCP/IP completo
  • Autenticação no banco
  • Alocação de memória no driver e no servidor
  • 100-200ms de overhead por conexão

Funciona... até não funcionar.

Com 1000 requisições por segundo, você tem 1000 conexões simultâneas. O PostgreSQL tem limites. O servidor tem limites. O resultado é previsível: crash.

A Solução: Connection Pool

A ideia é simples. Em vez de criar e destruir conexões a cada request, você mantém um conjunto de conexões já abertas e prontas pra uso.

// Com connection pool
Connection conn = dataSource.getConnection(); // <1ms - reutiliza conexão existente
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM transaction_events");
ResultSet rs = stmt.executeQuery();
// ... processar dados
conn.close(); // NÃO fecha realmente - devolve ao pool

A diferença é brutal:

  • <1ms pra obter uma conexão (já está criada)
  • Reutilização de conexões existentes
  • Controle sobre o número máximo de conexões abertas
  • Proteção do banco contra sobrecarga

O conn.close() aqui é enganoso. Ele não fecha a conexão de verdade. Ele devolve ao pool pro próximo que precisar.

Por Que HikariCP?

O Spring Boot escolheu o HikariCP como connection pool padrão por um motivo: ele é o mais rápido e leve do ecossistema Java.

O nome vem do japonês hikari (luz) - e não é exagero. Comparado com alternativas como C3P0 ou DBCP2, o HikariCP entrega latências significativamente menores com footprint de memória reduzido.

Se você usa Spring Boot com starter de JPA ou JDBC, o HikariCP já está lá. Sem configuração adicional. Mas usar o padrão é uma coisa. Configurar pra produção é outra.

Configuração: O Que Cada Parâmetro Realmente Faz

Tamanho do Pool

spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=20

minimum-idle=5 significa que o HikariCP mantém 5 conexões abertas o tempo todo, mesmo sem tráfego. São conexões "quentes", prontas pra responder imediatamente aos primeiros requests.

maximum-pool-size=20 é o limite. Se as 20 conexões estão ocupadas e chega o request 21, ele espera na fila. Esse limite existe pra proteger o PostgreSQL - um pool de 500 conexões não vai melhorar sua performance, vai matar seu banco.

Vale mencionar: a documentação do HikariCP recomenda configurar minimum-idle igual ao maximum-pool-size, criando um pool de tamanho fixo. Isso evita o overhead de criar e destruir conexões durante flutuações de carga. Na prática, uso pool dinâmico em ambientes com múltiplos serviços pra conservar recursos, mas pool fixo dá latência mais previsível sob carga sustentada.

Na prática, o dimensionamento depende do ambiente:

AmbienteMin IdleMax PoolJustificativa
DEV520Um developer, pouco tráfego
TST1030Testes automatizados, carga média
PP1540Simulação de produção
PRD2050Múltiplos handlers e tipos de transação

Um erro comum é configurar maximum-pool-size alto demais. O PostgreSQL tem seu próprio limite (tipicamente max_connections = 100 por padrão). Se você tem 4 instâncias da aplicação, cada uma com pool de 50, são 200 conexões competindo por 100 slots. Dimensione seus pools considerando a topologia completa de deploy.

Timeouts

spring.datasource.hikari.connection-timeout=30000      # 30 segundos
spring.datasource.hikari.idle-timeout=300000           # 5 minutos
spring.datasource.hikari.max-lifetime=1800000          # 30 minutos

Cada um tem um papel específico:

connection-timeout (30s) - Tempo máximo que uma thread espera por uma conexão livre. Se todas as 20 estão ocupadas e passam 30 segundos, o HikariCP lança exceção. Quando isso acontece, o problema geralmente é outro: queries lentas bloqueando conexões, pool subdimensionado ou connection leaks.

idle-timeout (5min) - Conexões ociosas além desse tempo são fechadas. Isso economiza recursos em períodos de baixo tráfego, mas o pool nunca fica abaixo do minimum-idle.

O comportamento na prática:

08:00 -> Sistema arranca -> cria 5 conexões (minimum-idle)
09:00 -> Pico de tráfego -> escala para 20 conexões
10:00 -> Tráfego normaliza -> apenas 3 conexões em uso
10:05 -> 12 conexões fechadas (ociosas há 5 min)
10:05 -> Ficam 5 conexões abertas (minimum-idle) + 3 em uso

max-lifetime (30min) - Toda conexão é marcada para reciclagem após 30 minutos. Porém, uma conexão em uso não é fechada forçadamente -- ela só é removida quando devolvida ao pool. Isso previne problemas reais: memory leaks no driver JDBC, conexões TCP em estado inconsistente e rotação de credenciais no PostgreSQL.

Detecção de Connection Leaks

spring.datasource.hikari.leak-detection-threshold=60000  # 60 segundos

Um connection leak acontece quando uma conexão é retirada do pool e nunca devolvida. O efeito é lento e silencioso: o pool vai esvaziando até travar a aplicação inteira.

O código clássico que causa isso:

@Service
public class BadService {
    @Autowired
    private DataSource dataSource;

    public void processTransaction() throws SQLException {
        Connection conn = dataSource.getConnection();
        // ... faz queries
        // ESQUECEU de conn.close()!
        // Conexão nunca volta ao pool
    }
}

Com leak-detection-threshold=60000, o HikariCP identifica qualquer conexão que ficou fora do pool por mais de 60 segundos e loga um warning com o stack trace completo. Você sabe exatamente onde o leak está.

A correção é direta. Use try-with-resources:

public void processTransaction() throws SQLException {
    try (Connection conn = dataSource.getConnection()) {
        // ... faz queries
    } // Fecha automaticamente, mesmo com exceção
}

Ou melhor ainda, deixe o Spring gerenciar com @Transactional:

@Service
public class TransactionService {
    @Autowired
    private TransactionRepository repository;

    @Transactional  // Spring abre e fecha conexão automaticamente
    public void processTransaction(TransactionEvent event) {
        repository.save(event);
    }
}

Otimizações para PostgreSQL

Além do pool em si, as propriedades do HikariCP permitem passar configurações diretamente pro driver JDBC do PostgreSQL (PgJDBC). Duas otimizações se destacam.

Server-Side Prepared Statements

O driver JDBC do PostgreSQL suporta prepared statements no lado do servidor, que pulam as fases de parse e planejamento pra queries executadas frequentemente. Por padrão, após uma PreparedStatement ser executada 5 vezes (prepareThreshold=5), o PgJDBC automaticamente promove ela pra um prepared statement no servidor. As execuções seguintes reutilizam o plano de execução já cacheado.

spring.datasource.hikari.data-source-properties.prepareThreshold=5
spring.datasource.hikari.data-source-properties.preparedStatementCacheQueries=256
spring.datasource.hikari.data-source-properties.preparedStatementCacheSizeMiB=5
  • prepareThreshold=5 -- Número de execuções antes do PgJDBC usar prepared statements no servidor (padrão: 5)
  • preparedStatementCacheQueries=256 -- Máximo de queries cacheadas no lado do cliente (padrão: 256)
  • preparedStatementCacheSizeMiB=5 -- Memória máxima pro cache no lado do cliente (padrão: 5 MiB)

Pra queries OLTP simples, o overhead de parse/plan é tipicamente 1-5ms. Pra queries complexas com múltiplos joins, pode chegar a 10-50ms. Em um sistema event-driven onde as mesmas queries executam milhares de vezes por minuto, server-side prepared statements eliminam esse overhead após as primeiras execuções.

Batch Inserts com reWriteBatchedInserts

Essa configuração é crítica pra quem usa padrões como Transactional Outbox.

spring.datasource.hikari.data-source-properties.reWriteBatchedInserts=true

Sem rewrite, inserir 100 registros na tabela outbox significa 100 round-trips individuais ao banco:

INSERT INTO outbox (id, topic, payload) VALUES ('1', 'order.created', '...');
INSERT INTO outbox (id, topic, payload) VALUES ('2', 'order.created', '...');
-- ... 98 mais ...

Com reWriteBatchedInserts=true, o driver JDBC reescreve automaticamente pra um único statement:

INSERT INTO outbox (id, topic, payload) VALUES
  ('1', 'order.created', '...'),
  ('2', 'order.created', '...'),
  -- ... 98 mais ...
  ('100', 'order.created', '...');

A melhoria depende da latência de rede entre aplicação e PostgreSQL. Em ambientes de alta latência (cross-region ou cloud), a redução pode ser dramática -- de segundos pra centenas de milissegundos. Em ambientes de baixa latência (mesmo datacenter), o ganho absoluto é menor, mas a melhoria relativa ainda é significativa. Se você persiste eventos em tabela de outbox, essa otimização é obrigatória.

Na Prática: Fluxo em um Sistema Event-Driven

Pra deixar concreto, veja como tudo funciona junto em um sistema real que processa eventos do Solace.

@Component
public class OrderCreatedConsumer extends AbstractConsumer {

    private final EventStoreService eventStoreService;

    @Override
    public void process(final Message message) {
        final var event = parse(message);
        eventStoreService.append(event, "order.created");
    }
}

@Service
public class EventStoreService {

    private final TransactionEventRepository eventRepo;
    private final OutboxRepository outboxRepo;

    @Transactional  // Spring pede conexão ao HikariCP
    public void append(TransactionEvent event, String topic) {
        // HikariCP entrega conexão idle em <1ms
        var eventEntity = toEntity(event);
        var outboxEntry = toOutboxEntry(event, topic);

        eventRepo.save(eventEntity);    // INSERT com prepared stmt no servidor
        outboxRepo.save(outboxEntry);   // INSERT com prepared stmt no servidor

        // COMMIT -> conexão volta ao pool
    }
}

A timeline detalhada:

t=0ms    -> Evento chega ao handler
t=1ms    -> @Transactional pede conexão ao HikariCP
t=1ms    -> HikariCP entrega conexão #7 (estava idle)
t=2ms    -> BEGIN transaction no PostgreSQL
t=5ms    -> INSERT transaction_events (prepared stmt em cache)
t=8ms    -> INSERT outbox (prepared stmt em cache)
t=10ms   -> COMMIT transaction
t=11ms   -> Conexão devolvida ao pool

Total: 11ms (vs. 100ms+ sem pool)

De 100ms+ pra 11ms. Isso é o que separa uma aplicação que aguenta carga de uma que trava.

Monitoramento: Saber Antes de Quebrar

Configurar o pool é só metade do trabalho. Em produção, você precisa monitorar.

Com Spring Boot Actuator, as métricas do HikariCP ficam disponíveis via endpoint:

curl http://localhost:8080/actuator/metrics/hikaricp.connections.active

As métricas que importam:

MétricaSignificadoValor Saudável
hikaricp.connections.activeConexões em uso agora< 80% do max
hikaricp.connections.idleConexões disponíveis> 20% do max
hikaricp.connections.pendingThreads esperando conexão0
hikaricp.connections.timeoutTimeouts acumulados0

Três alertas que você deve configurar no Grafana/Prometheus:

  • active > 90% do max -- Pool quase esgotado
  • pending > 5 -- Threads bloqueadas esperando
  • timeout > 0 -- Connection timeouts acontecendo

Troubleshooting: Os Problemas Mais Comuns

"Connection is not available, request timed out"

HikariPool-1 - Connection is not available, request timed out after 30000ms.

Esse é o erro mais comum. Pool esgotado. Diagnóstico:

  1. Verifique hikaricp.connections.active - se está no máximo, o pool está pequeno ou tem queries lentas
  2. Verifique hikaricp.connections.pending - se maior que zero, threads estão bloqueadas
  3. Investigue queries lentas no PostgreSQL:
docker exec -it postgres psql -U admin -d mydb \
  -c "SELECT pid, now() - query_start as duration, query
      FROM pg_stat_activity
      WHERE state = 'active'
      ORDER BY duration DESC;"

Conexões sendo criadas e destruídas constantemente

Se você vê no log:

HikariPool-1 - Added connection conn10
HikariPool-1 - Closing connection conn10 (idle timeout)
HikariPool-1 - Added connection conn11

O minimum-idle está baixo demais ou o idle-timeout está curto demais. Aumente ambos:

spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.idle-timeout=600000  # 10 minutos

Memory Leak por Connection Leak

Se a memória da aplicação cresce constantemente, ative leak detection agressivo temporariamente:

spring.datasource.hikari.leak-detection-threshold=10000  # 10 segundos

Procure no log por Connection leak detected, analise o stack trace e corrija com @Transactional ou try-with-resources.

Boas Práticas - Resumo

Faça:

  • Use @Transactional - deixe o Spring gerenciar conexões
  • Use try-with-resources quando precisar de conexões manuais
  • Monitore métricas do pool via Actuator + Grafana
  • Dimensione o pool considerando a topologia completa de deploy
  • Ative leak detection em todos os ambientes

Não faça:

  • Nunca esqueça de fechar conexões manuais
  • Nunca bloqueie conexões com Thread.sleep() dentro de @Transactional
  • Nunca configure maximum-pool-size=500 achando que mais é melhor
  • Nunca ignore warnings de connection leak no log

Conclusão

Connection pooling não é um detalhe de configuração. É uma decisão arquitetural que impacta diretamente a performance e estabilidade da sua aplicação em produção.

O HikariCP faz um trabalho excelente como padrão do Spring Boot, mas entender o que cada parâmetro faz e como monitorar é o que separa uma configuração que funciona em dev de uma que sobrevive em produção.

Nos próximos posts, vou explorar mais a fundo padrões como Transactional Outbox e Event Sourcing com PostgreSQL, onde o connection pooling se torna ainda mais crítico.

Se você quer trocar ideias sobre connection pooling, performance com PostgreSQL ou arquitetura event-driven, me procure no LinkedIn. Adoro essas conversas.


Recursos para aprofundar:

  • HikariCP GitHub
  • HikariCP - Down the Rabbit Hole
  • PostgreSQL - Number of Database Connections
  • Spring Boot DataSource Configuration
Tags:
JavaSpring BootHikariCPPostgreSQLConnection Pooling
  • Política de Privacidade
  • Termos de Serviço
  • Contato
© 2025 Programming On Mars. Todos os direitos reservados.