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 > 0por períodos sustentados: pool esgotadohikaricp.connections.timeout > 0crescendo: requests falhando por falta de conexãohikaricp.connections.active == hikaricp.connections.maxconstantemente: pool no limitehikaricp.connections.usagemuito 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 bancoCm= 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.