Programming on Mars
/
JavaSpring BootHikariCPPostgreSQLConnection Pooling

HikariCP com Spring Boot e PostgreSQL: Configuração e Diagnóstico em Produção

Como configurar o HikariCP com Spring Boot e PostgreSQL em produção: fórmula de dimensionamento, keepalive para cloud e diagnóstico com pg_stat_activity.

André Lucas

February 15, 2026

HikariCP com Spring Boot e PostgreSQL: Configuração e Diagnóstico em Produção

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

Se você chegou até aqui com esse erro no log, seu pool está esgotado. A boa notícia: dá pra resolver. A má notícia: provavelmente a configuração padrão não é suficiente pra produção.

Trabalhando com sistemas event-driven que processam milhares de transações por segundo, aprendi que como você configura conexões com o PostgreSQL pode ser a diferença entre uma resposta em 11ms e um travamento 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

Fórmula de Dimensionamento

Mais do que tabela de referência, existe uma fórmula documentada pelo próprio HikariCP para calcular o tamanho ideal do pool:

((número_de_cores × 2) + número_efetivo_de_discos)

Exemplo concreto: servidor com 4 cores e 1 disco SSD (spindle count efetivo = 1):

((4 × 2) + 1) = 9 conexões por instância

Essa é a recomendação para maximizar throughput sem saturar o banco. Mais conexões além desse número geram contenção, não throughput.

Em ambientes cloud com múltiplas instâncias, o total de conexões no PostgreSQL é:

resultado_da_fórmula × número_de_pods/instâncias

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. Para estruturar banco e migrations desde o início nessa topologia, o Lab de Migrations com Flyway é um bom ponto de partida.

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
spring.datasource.hikari.keepalive-time=120000         # 2 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.

keepalive-time (2min) - Esse parâmetro é crítico em ambientes cloud. AWS, GCP e Azure silenciosamente derrubam conexões TCP ociosas após alguns minutos — tipicamente 4 a 8 minutos, dependendo de security groups e load balancers. O HikariCP não sabe que a conexão está morta até tentar usá-la, e aí aparece aquele Connection is not available inesperado no momento mais inoportuno.

Com keepalive-time=120000, o HikariCP envia keepalive probes nas conexões idle antes que o cloud provider as derrube, mantendo-as vivas. Em ambientes on-premise: dispensável. Em qualquer cloud: obrigatório.

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 em sistemas de Arquitetura Orientada a Eventos.

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. O código usa o padrão Transactional Outbox — se você quer ver esse padrão completo em ação, confira o lab Arquitetura Orientada a Eventos na Prática com Kafka e PostgreSQL, onde aplicamos essas configurações num projeto com Dual Write e Chaos Testing.

@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.

Pool esgotado. O diagnóstico correto começa pelas métricas, mas o culpado real costuma estar no banco.

  1. Verifique hikaricp.connections.active — se está no máximo, o pool está pequeno ou tem conexões travadas
  2. Verifique hikaricp.connections.pending — se maior que zero, threads estão bloqueadas

O próximo passo é inspecionar o que está acontecendo no lado do PostgreSQL. A query abaixo mostra tanto queries ativas lentas quanto o estado idle in transaction — que é frequentemente a causa real do pool esgotado:

-- Diagnóstico completo: detecta idle in transaction (o problema mais comum)
SELECT pid,
       now() - pg_stat_activity.query_start AS duration,
       query,
       state
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > interval '5 seconds'
  AND state IN ('active', 'idle in transaction')
ORDER BY duration DESC;

O estado idle in transaction significa que a conexão está presa dentro de uma transação aberta mas sem executar nenhuma query. Não aparece se você filtrar só por state = 'active' — e é justamente por isso que tantos diagnósticos ficam no escuro.

Causas mais comuns: Thread.sleep() ou chamada HTTP externa dentro de um @Transactional, ou exceção não tratada que deixou a transação aberta sem rollback. Cada uma dessas conexões presas ocupa um slot do pool permanentemente, até o max-lifetime expirar ou a aplicação reiniciar.

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
  • Configure keepalive-time em qualquer ambiente cloud

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.

Comece hoje: abra seu application.yml e adicione keepalive-time=120000 e leak-detection-threshold=60000. Dois parâmetros, cinco minutos — e você previne a maioria dos problemas mais comuns em produção.

Se você quer ver o HikariCP em ação dentro de um sistema event-driven real, confira o Lab de Arquitetura Orientada a Eventos com Kafka e PostgreSQL — onde aplicamos todas essas configurações num projeto completo com Dual Write e Chaos Testing. Para estruturar banco e migrations na mesma topologia, o Lab de Migrations com Flyway é o ponto de partida.

Publico um post por semana sobre Java, Spring Boot e arquitetura event-driven. Siga no LinkedIn para não perder.


Recursos para aprofundar:

Tags

JavaSpring BootHikariCPPostgreSQLConnection Pooling
← Back to home