|

Connection Pool Exhaustion: Quando a Aplicação Trava por Falta de Conexões

Existe uma falha de produção com uma assinatura muito específica. A aplicação está no ar. O banco de dados está saudável. A CPU não está alta. A memória está ok. Mas os requests começam a demorar cada vez mais, o timeout do load balancer começa a disparar, e o log enche de uma exceção:

Unable to acquire JDBC Connection
HikariPool-1 - Connection is not available, request timed out after 30000ms

O banco tem capacidade. A aplicação tem memória. O que não tem são conexões disponíveis no pool. Cada conexão existente está ocupada, e cada novo request fica na fila esperando uma ser liberada — até o timeout.

Connection pool exhaustion não é um problema de infraestrutura. É um problema de código: conexões sendo mantidas abertas por mais tempo do que deveriam, pool subdimensionado para a carga real, ou os dois ao mesmo tempo.


Por que Connection Pools Existem

Estabelecer uma conexão JDBC com um banco de dados é caro. O processo envolve handshake TCP, autenticação, negociação de protocolo e alocação de recursos no servidor de banco. Em PostgreSQL, cada conexão é um processo separado no sistema operacional. O custo de criar uma conexão nova pode variar entre 20ms e 200ms — inaceitável para cada query de uma aplicação web.

O connection pool resolve isso mantendo um conjunto de conexões já estabelecidas, prontas para uso imediato. Quando um request precisa de uma conexão, pega uma do pool. Quando termina, devolve. A criação acontece uma vez; o reuso acontece milhares de vezes.

O problema é que esse mecanismo pressupõe que conexões são devolvidas rapidamente. Quando não são — por qualquer razão — o pool esgota.

HikariCP: o pool padrão no ecossistema Spring

O HikariCP é o connection pool padrão do Spring Boot desde a versão 2.0. É consistentemente o mais rápido em benchmarks e tem configurações sensatas como padrão. Os conceitos deste artigo se aplicam a outros pools (c3p0, DBCP2, Tomcat Pool), mas os exemplos de configuração são em HikariCP.

properties

# Configuração padrão do HikariCP no Spring Boot
spring.datasource.hikari.maximum-pool-size=10      # máximo de conexões no pool
spring.datasource.hikari.minimum-idle=10           # mínimo mantido ocioso
spring.datasource.hikari.connection-timeout=30000  # tempo máximo esperando conexão (ms)
spring.datasource.hikari.idle-timeout=600000       # tempo máximo de conexão ociosa (ms)
spring.datasource.hikari.max-lifetime=1800000      # tempo máximo de vida de uma conexão (ms)

O padrão de maximum-pool-size=10 é frequentemente a primeira configuração que precisa ser revisada — mas aumentar o pool sem entender a causa raiz é um paliativo, não uma solução.


As Causas

Causa 1: Transações mantidas abertas durante I/O externo

A causa mais comum e mais devastadora. Uma transação @Transactional que faz uma chamada HTTP, envia um email, ou publica em uma fila durante sua execução mantém a conexão do banco ocupada durante toda a duração dessa chamada externa.

java

@Service
public class OrderService {

    @Transactional // Abre conexão aqui
    public void processOrder(Long orderId) {

        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus("PROCESSING");
        orderRepository.save(order);

        // PROBLEMA: conexão do banco fica aberta durante toda esta chamada
        // Se o serviço externo demorar 2 segundos, a conexão fica presa por 2 segundos
        notificationService.sendEmail(order.getCustomer().getEmail()); // HTTP externo

        // PROBLEMA: mesmo aqui
        paymentGateway.authorize(order.getPaymentToken()); // HTTP externo ~500ms

        order.setStatus("CONFIRMED");
        orderRepository.save(order);

    } // Conexão liberada apenas aqui
}

Se sendEmail demora 500ms e paymentGateway.authorize demora 1 segundo, cada execução deste método mantém uma conexão ocupada por ~1.5 segundos além do necessário. Com 10 conexões no pool e 10 requests simultâneos, o pool esgota em menos de 2 segundos de carga.

Causa 2: @Transactional em métodos que não precisam de transação

@Transactional é frequentemente adicionado por hábito ou por precaução em métodos que só leem dados, em métodos que não acessam o banco de jeito nenhum, ou em camadas erradas da aplicação.

java

@Service
public class ReportService {

    // @Transactional desnecessário: apenas lê, poderia ser readOnly
    // ou nem precisar de transação se for uma única query
    @Transactional
    public List<ReportDTO> generateReport(DateRange range) {
        List<ReportDTO> data = reportRepository.findByDateRange(range);

        // Processamento puramente em memória: não precisa de transação aberta
        return data.stream()
            .filter(dto -> dto.getTotal().compareTo(BigDecimal.ZERO) > 0)
            .sorted(Comparator.comparing(ReportDTO::getTotal).reversed())
            .collect(toList());
    }

    // @Transactional em método que não acessa banco
    @Transactional
    public String formatCurrency(BigDecimal value) {
        return NumberFormat.getCurrencyInstance().format(value); // sem banco
    }
}

Causa 3: Conexões não liberadas por exceção sem rollback adequado

Se uma exceção é lançada dentro de um contexto transacional e não é tratada corretamente, o Spring pode não liberar a conexão imediatamente:

java

@Transactional
public void importData(List<RecordDTO> records) {
    for (RecordDTO record : records) {
        try {
            processRecord(record); // pode lançar exceção de validação
        } catch (ValidationException e) {
            log.warn("Skipping invalid record: {}", record.getId());
            // Transação marcada como rollback-only mas não finalizada
            // A conexão continua presa até o fim do método
        }
    }
}

Quando uma exceção é capturada dentro de um método @Transactional, o Spring marca a transação como rollback-only. Você pode continuar executando código, mas a transação já está comprometida — e a conexão continua ocupada.

Causa 4: Pool subdimensionado para a concorrência real

A matemática do pool é direta mas frequentemente ignorada. Se cada request usa uma conexão por 50ms em média, e você tem 10 conexões, o throughput máximo teórico é 10 conexões / 0.05s = 200 requests/segundo. Acima disso, requests começam a esperar.

Mas a maioria das aplicações tem operações com durações muito diferentes: algumas queries levam 2ms, algumas transações complexas levam 500ms. A distribuição importa tanto quanto a média.

Causa 5: N+1 e chatty queries como multiplicador de tempo de conexão

N+1 queries e chatty queries não apenas aumentam o número de queries — elas aumentam o tempo que a conexão fica ocupada. Uma operação que deveria levar 5ms com JOINs adequados pode levar 200ms com N+1. Isso multiplica o tempo de posse da conexão por 40x.

Com pool de 10 conexões e 10 requests simultâneos de 200ms cada: o pool fica saturado. Com as mesmas 10 conexões e os mesmos requests corrigidos para 5ms: 10 conexões suportam 2.000 requests/segundo.

Causa 6: Deadlock no banco travando conexões indefinidamente

Um deadlock no banco faz duas transações esperarem uma pela outra indefinidamente — ou até o timeout de deadlock do banco, que pode ser de 30 segundos a vários minutos. Cada transação em deadlock mantém sua conexão ocupada durante todo esse tempo.

java

// Thread A
@Transactional
public void transferA(Long from, Long to, BigDecimal amount) {
    Account accountFrom = accountRepository.findById(from); // lock em 'from'
    Account accountTo = accountRepository.findById(to);     // espera lock em 'to'
    // ...
}

// Thread B (executando simultaneamente)
@Transactional
public void transferB(Long from, Long to, BigDecimal amount) {
    Account accountFrom = accountRepository.findById(to);   // lock em 'to'
    Account accountTo = accountRepository.findById(from);   // espera lock em 'from'
    // deadlock
}

Causa 7: Leak de conexão — conexão nunca devolvida ao pool

O caso mais raro em código moderno com Spring, mas ainda possível: código que obtém uma conexão diretamente via DataSource.getConnection() e não a fecha adequadamente.

java

// Conexão vazando: nunca fechada em caso de exceção
public void riskyOperation() throws SQLException {
    Connection conn = dataSource.getConnection(); // obtém conexão
    Statement stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT ...");
    // Se uma exceção ocorrer aqui, conn nunca é fechada
    processResults(rs);
    conn.close(); // nunca alcançado em caso de exceção
}

// Correto: try-with-resources garante fechamento
public void safeOperation() throws SQLException {
    try (Connection conn = dataSource.getConnection();
         Statement stmt = conn.createStatement();
         ResultSet rs = stmt.executeQuery("SELECT ...")) {
        processResults(rs);
    } // conn fechada automaticamente, mesmo com exceção
}

Diagnóstico

Métricas do HikariCP via Micrometer

O HikariCP expõe métricas automaticamente quando Micrometer está no classpath (padrão em projetos Spring Boot com Actuator):

xml

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

properties

# application.properties
management.endpoints.web.exposure.include=health,metrics,prometheus
management.metrics.enable.hikaricp=true

Métricas expostas em /actuator/metrics/hikaricp.*:

hikaricp.connections.active      # conexões em uso agora
hikaricp.connections.idle        # conexões disponíveis
hikaricp.connections.pending     # threads esperando conexão
hikaricp.connections.timeout     # total de timeouts (contador)
hikaricp.connections.acquire     # tempo médio para adquirir conexão
hikaricp.connections.usage       # tempo médio que a conexão ficou em uso
hikaricp.connections.creation    # tempo médio para criar nova conexão

Sinais de alerta:

  • hikaricp.connections.pending > 0 por períodos sustentados: pool esgotado
  • hikaricp.connections.timeout > 0 crescendo: requests falhando por falta de conexão
  • hikaricp.connections.active == hikaricp.connections.max constantemente: pool no limite
  • hikaricp.connections.usage muito alto (>100ms): conexões presas por muito tempo

Configure alertas para connections.pending > 0 por mais de 10 segundos e para connections.timeout acima de zero.

Habilitando leak detection no HikariCP

O HikariCP tem um mecanismo de detecção de leaks que loga um warning se uma conexão for mantida por mais tempo do que o threshold configurado:

properties

# Loga stack trace se uma conexão for mantida por mais de 2 segundos
spring.datasource.hikari.leak-detection-threshold=2000

O log gerado:

WARN  HikariPool-1 - Connection leak detection triggered for
      com.zaxxer.hikari.pool.ProxyConnection@1a2b3c4d,
      stack trace follows:
      java.lang.Exception: Apparent connection leak detected
          at com.example.OrderService.processOrder(OrderService.java:45)
          at com.example.OrderController.process(OrderController.java:23)
          ...

Isso aponta exatamente onde a conexão foi adquirida e não devolvida no tempo esperado — a forma mais rápida de encontrar o código problemático.

Monitorando transações longas no banco

PostgreSQL:

sql

-- Transações abertas há mais de 30 segundos
SELECT
    pid,
    now() - xact_start AS duration,
    query,
    state,
    wait_event_type,
    wait_event
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
  AND now() - xact_start > interval '30 seconds'
ORDER BY duration DESC;

sql

-- Locks sendo esperados (possível deadlock ou contenção)
SELECT
    blocked.pid AS blocked_pid,
    blocked.query AS blocked_query,
    blocking.pid AS blocking_pid,
    blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
    ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.cardinality(pg_blocking_pids(blocked.pid)) > 0;

MySQL:

sql

-- Transações longas
SELECT
    trx_id,
    trx_started,
    TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_seconds,
    trx_query,
    trx_state
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 30
ORDER BY duration_seconds DESC;

Teste de carga para reproduzir o problema

java

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ConnectionPoolTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void poolNaoDeveEsgotarCom50RequestsSimultaneos() throws InterruptedException {
        int concurrency = 50;
        CountDownLatch latch = new CountDownLatch(concurrency);
        List<Integer> statusCodes = Collections.synchronizedList(new ArrayList<>());

        ExecutorService executor = Executors.newFixedThreadPool(concurrency);

        for (int i = 0; i < concurrency; i++) {
            executor.submit(() -> {
                ResponseEntity<String> response =
                    restTemplate.getForEntity("/api/orders/1", String.class);
                statusCodes.add(response.getStatusCodeValue());
                latch.countDown();
            });
        }

        latch.await(30, TimeUnit.SECONDS);
        executor.shutdown();

        // Nenhum request deveria falhar com 503 (pool exhausted)
        long failures = statusCodes.stream()
            .filter(code -> code >= 500)
            .count();

        assertThat(failures).isZero();
    }
}

As Soluções

1. Mover I/O externo para fora da transação

A correção mais importante. A regra é direta: transações devem conter apenas operações de banco de dados. Qualquer I/O externo (HTTP, email, fila, filesystem) deve acontecer fora do escopo transacional.

java

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final NotificationService notificationService;
    private final PaymentGateway paymentGateway;

    // Método transacional: apenas operações de banco
    @Transactional
    public Order markOrderAsProcessing(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus("PROCESSING");
        return orderRepository.save(order);
    }

    @Transactional
    public Order confirmOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus("CONFIRMED");
        return orderRepository.save(order);
    }

    // Orquestrador: sem @Transactional, coordena I/O e banco
    public void processOrder(Long orderId) {

        // Operação de banco: transação abre e fecha rapidamente
        Order order = markOrderAsProcessing(orderId);

        // I/O externo: fora de qualquer transação
        notificationService.sendEmail(order.getCustomer().getEmail()); // 500ms, sem conexão presa

        PaymentResult result = paymentGateway.authorize(
            order.getPaymentToken()
        ); // 1000ms, sem conexão presa

        if (result.isApproved()) {
            // Operação de banco: nova transação rápida
            confirmOrder(orderId);
        }
    }
}

Atenção ao self-invocation: chamar um método @Transactional do mesmo bean não passa pelo proxy Spring, então a transação não é criada. Para o padrão acima funcionar, os métodos transacionais precisam ser chamados através do proxy — ou usar @Autowired na própria classe (um anti-pattern que funciona), ou separar em beans diferentes.

java

// Forma limpa: separar em dois beans
@Service
public class OrderTransactionalService {

    @Transactional
    public Order markAsProcessing(Long orderId) { ... }

    @Transactional
    public Order confirm(Long orderId) { ... }
}

@Service
public class OrderOrchestrator {

    private final OrderTransactionalService transactionalService;
    private final NotificationService notificationService;
    private final PaymentGateway paymentGateway;

    public void processOrder(Long orderId) {
        Order order = transactionalService.markAsProcessing(orderId);
        notificationService.sendEmail(order.getCustomer().getEmail());
        PaymentResult result = paymentGateway.authorize(order.getPaymentToken());
        if (result.isApproved()) {
            transactionalService.confirm(orderId);
        }
    }
}

2. @Transactional(readOnly = true) para operações de leitura

Para métodos que apenas leem dados, readOnly = true desabilita dirty checking, flush automático e, em alguns bancos e drivers, pode rotear para réplicas de leitura. Mais importante: torna a intenção explícita — este método não deve escrever.

java

@Transactional(readOnly = true)
public List<OrderSummaryDTO> findOrdersByCustomer(Long customerId) {
    return orderRepository.findSummariesByCustomerId(customerId);
}

@Transactional(readOnly = true)
public Optional<OrderDetailDTO> findOrderDetail(Long orderId) {
    return orderRepository.findOrderDetail(orderId);
}

Para queries que são uma única operação sem necessidade de consistência entre múltiplas leituras, você pode dispensar o @Transactional completamente — o Spring Data JPA abre e fecha uma transação automaticamente por query de repositório.

3. Dimensionamento correto do pool

O sizing do pool não é arbitrário. Tem uma fórmula baseada no trabalho de Brendan Gregg e popularizada pelo próprio autor do HikariCP:

pool_size = Tn × (Cm - 1) + 1

Onde:

  • Tn = número máximo de threads concorrentes que acessam o banco
  • Cm = número máximo de conexões simultâneas que uma única thread pode precisar (normalmente 1, exceto em casos de nested transactions)

Para a maioria das aplicações web, isso simplifica para: o pool deve ter tantas conexões quantas threads do servidor web podem estar ativamente executando queries ao mesmo tempo.

Mas há um limite superior importante: o banco de dados tem um limite de conexões simultâneas. PostgreSQL por padrão aceita 100 conexões. Com múltiplas instâncias da aplicação, o pool de cada instância deve ser dimensionado para que instâncias × pool_size < max_connections_do_banco.

properties

# Exemplo: 3 instâncias da aplicação, PostgreSQL com max_connections=100
# Pool por instância: 100 / 3 = ~33, com margem para ferramentas de admin
spring.datasource.hikari.maximum-pool-size=25
spring.datasource.hikari.minimum-idle=5

# Timeout razoável: falha rápido em vez de esperar 30 segundos
spring.datasource.hikari.connection-timeout=5000

# Detecção de leak em desenvolvimento
spring.datasource.hikari.leak-detection-threshold=2000

Contra-intuitivo mas importante: aumentar o pool além do necessário pode piorar a performance. Mais conexões simultâneas significa mais contenção no banco (locks, shared memory, CPU do banco distribuída entre mais processos). O paper “Why you don’t need more than one connection pool” de HikariCP explora isso em detalhe. Comece conservador e aumente com base em métricas reais.

4. Timeout de transação para limitar danos

Configure um timeout de transação para garantir que nenhuma transação fique aberta indefinidamente — mesmo que o código tenha um bug:

java

@Transactional(timeout = 5) // máximo 5 segundos
public void processOrder(Long orderId) {
    // Se isso demorar mais de 5 segundos, TransactionTimedOutException
}

properties

# Timeout global para todas as transações gerenciadas pelo Spring
spring.transaction.default-timeout=10

Combine com timeout no próprio HikariCP:

properties

# Máximo que uma conexão pode ficar em uso (independente de timeout de transação)
spring.datasource.hikari.keepalive-time=30000

5. Async para I/O que não precisa de resultado imediato

Para operações como envio de email, notificações, auditoria — onde o resultado não afeta a resposta ao usuário — use processamento assíncrono para liberar o request (e a conexão) imediatamente:

java

@Service
public class OrderService {

    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        Order order = new Order(request);
        Order saved = orderRepository.save(order);

        // Dispara assincronamente: não bloqueia, não mantém conexão
        eventPublisher.publishEvent(new OrderCreatedEvent(saved.getId()));

        return saved;
    } // Transação commita e conexão é devolvida aqui
}

@Component
public class OrderEventHandler {

    @Async // Executa em thread separada, fora da transação original
    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        notificationService.sendConfirmationEmail(event.getOrderId()); // HTTP externo
        inventoryService.reserveItems(event.getOrderId());             // pode ter sua própria transação
    }
}

Configure um thread pool dedicado para @Async para não usar o pool de threads do servidor web:

java

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "asyncTaskExecutor")
    public Executor asyncTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
}

6. Resolver N+1 e chatty queries para reduzir tempo de posse de conexão

Como discutido nos artigos anteriores da série, cada N+1 e cada chatty query aumenta o tempo que a conexão fica em uso. Resolver esses problemas reduz o hikaricp.connections.usage — o que diretamente aumenta o throughput do pool sem adicionar conexões.

Conexão ocupada por 200ms (N+1) × 10 conexões = 50 requests/s máximo
Conexão ocupada por 5ms (JOIN) × 10 conexões = 2.000 requests/s máximo

O pool correto e queries otimizadas se multiplicam.

7. PgBouncer para escalar além dos limites do PostgreSQL

O PostgreSQL cria um processo OS por conexão. Com 200+ conexões, a contenção entre processos começa a degradar a performance do próprio banco. Para aplicações que genuinamente precisam de muitas conexões (múltiplas instâncias, microserviços), um connection pooler no nível do banco é a solução correta.

O PgBouncer fica entre a aplicação e o PostgreSQL, multiplexando muitas conexões de aplicação em poucas conexões reais ao banco:

ini

; pgbouncer.ini
[databases]
mydb = host=localhost port=5432 dbname=mydb

[pgbouncer]
listen_port = 6432
listen_addr = localhost
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt

; Modo transaction: conexão é devolvida ao pool após cada transação
; Ideal para aplicações web com transações curtas
pool_mode = transaction

; Máximo de conexões reais ao PostgreSQL
server_pool_size = 20

; Máximo de conexões aceitas de clientes
max_client_conn = 1000

properties

# application.properties: aponta para PgBouncer, não para Postgres diretamente
spring.datasource.url=jdbc:postgresql://localhost:6432/mydb

No modo transaction, o PgBouncer devolve a conexão ao pool do banco assim que a transação commita — mesmo que o cliente ainda esteja conectado. 1.000 clientes podem compartilhar 20 conexões reais ao PostgreSQL.

Limitação do PgBouncer no modo transaction: prepared statements com nomes fixos não funcionam. Configure o driver JDBC para não usar prepared statements nomeados:

properties

spring.datasource.url=jdbc:postgresql://localhost:6432/mydb?prepareThreshold=0

O Ciclo de Morte por Pool Exhaustion

Vale entender a sequência completa de como o pool exhaustion degenera em indisponibilidade, porque ela explica por que o problema parece se manifestar de repente mesmo que a causa exista há tempo:

1. Tráfego aumenta gradualmente
2. Mais requests simultâneos → mais conexões em uso ao mesmo tempo
3. Pool chega ao máximo (active == maximum-pool-size)
4. Novos requests entram na fila esperando conexão
5. Requests na fila demoram mais → usuários percebem lentidão
6. Requests lentos acumulam → mais conexões presas (timeouts de downstream)
7. Fila cresce → connection-timeout começa a disparar
8. Exceptions chegam ao usuário → retry automático de clientes piora a carga
9. Cascata completa: aplicação parece travada mesmo com banco saudável

O passo crítico é o 6: conexões longas aumentam o tempo de posse, o que reduz o throughput efetivo do pool, o que aumenta a fila, o que aumenta o tempo de resposta, o que aumenta o tempo de posse das conexões restantes. É um loop de feedback positivo que se acelera sozinho.


Configuração de Referência para Produção

properties

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

# Timeouts
spring.datasource.hikari.connection-timeout=5000       # falha rápido: 5s esperando conexão
spring.datasource.hikari.idle-timeout=300000           # remove conexões ociosas após 5 min
spring.datasource.hikari.max-lifetime=1800000          # recria conexões a cada 30 min (evita conexões stale)
spring.datasource.hikari.keepalive-time=60000          # envia keepalive a cada 1 min

# Diagnóstico (desabilitar em produção de alto volume, habilitar ao investigar)
spring.datasource.hikari.leak-detection-threshold=5000 # warning se conexão presa >5s

# Nome do pool (aparece nos logs e métricas)
spring.datasource.hikari.pool-name=HikariPool-Main

# Transações
spring.transaction.default-timeout=30                  # timeout global de 30s
spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true

Conclusão

Connection pool exhaustion é o problema onde todos os outros se encontram. N+1 queries mantêm conexões presas por mais tempo do que deveriam. Chatty queries multiplicam o tempo de uso. Transações com I/O externo seguram conexões por segundos enquanto aguardam sistemas externos. Pool subdimensionado não tem margem para absorver nenhum desses problemas.

A solução não é uma única mudança — é a combinação de quatro práticas:

Primeiro, manter transações curtas e focadas em operações de banco. Segundo, mover I/O externo para fora do escopo transacional. Terceiro, dimensionar o pool com base em métricas reais, não em um número arbitrário. Quarto, monitorar connections.pending, connections.timeout e connections.usage continuamente — esses números contam a história antes que o sistema entre em colapso.

O connection pool é o recurso mais contendido em aplicações Java com banco de dados relacional. Tratá-lo como um detalhe de infraestrutura que “funciona sozinho” é o caminho para a próxima crise de produção às 3 da manhã.


Próximo da série: Long-running Transactions — como transações abertas por tempo demais afetam o banco além do connection pool, incluindo lock contention, vacuum bloat no PostgreSQL e estratégias de design transacional.

Posts Similares

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *