Usamos cookies essenciais para login e armazenamento local para suas preferências. Para métricas de uso, usamos o Google Analytics 4 — você pode recusar sem perder nada do produto. Saber mais

ThreadMineThreadMine
Voltar

Guia técnico

Spring Boot com threads travadas: como diagnosticar?

Pool Tomcat esgotado, requisições que não retornam, latência que só cresce. O caminho rápido entre o sintoma e a causa raiz com thread dump.

Testar grátis

Em produtos Spring Boot, "thread travada" raramente significa thread quebrada. Significa thread que entrou em uma chamada e não saiu mais — e cada nova requisição toma uma thread nova do pool, até o ponto em que não sobra nenhuma. O sintoma clássico é familiar: o endpoint que você está testando começa rápido, depois fica lento, depois para de responder completamente. O servidor não caiu; ele ficou sem braço livre para atender.

Esse padrão tem um nome em virtualmente todo container Java moderno — Tomcat chama de stuck thread, WebLogic chama de hogging thread, JBoss chama de blocked thread. Em Spring Boot, normalmente roda sobre Tomcat ou Undertow, e a sigla na log é org.apache.catalina.valves.StuckThreadDetectionValve. Esse valve, quando ativo, emite WARN com a pilha completa da thread que está presa há mais que threshold segundos.

Este guia mostra como tirar conclusão do log dele e do thread dump, sem chute, em uma sequência repetível.

O sintoma de uma stuck thread

Em Tomcat, a evidência mais explícita aparece no log da aplicação:

WARN  o.a.c.v.StuckThreadDetectionValve - Thread "http-nio-8080-exec-23"
   (id=87) has been active for 60011 milliseconds (since 2026-05-20T14:12:08Z)
   and may be stuck (configured threshold for this StuckThreadDetectionValve is 60 seconds).
   There is/are 5 thread(s) in total in the JVM which are monitored by this Valve and may be stuck.

Cinco threads do pool já passaram do limiar de 60 segundos. Se o pool tem 200 threads (default do Tomcat), você ainda tem folga; se sobe pra 20, já era. A latência P99 vai deteriorar antes do incidente ficar visível para o cliente.

As quatro causas mais comuns

1. Pool HTTP exausto por chamada externa sem timeout

Spring Boot por default não define timeout em RestTemplate nem em WebClient sem você configurar explicitamente. Uma API externa que responde em 200ms na maior parte do tempo e em 2 minutos sob carga vai gerar stuck threads imediatamente.

2. Conexão de banco pendurada

Query pesada, lock no Postgres, transação órfã, ou pool DBCP esgotado. A thread fica em socketRead0 na pilha JDBC, sem responder à interrupção.

3. Sincronização excessiva

Bloco synchronized em torno de IO ou trecho longo. Uma thread segura o monitor por segundos e todas as outras se enfileiram. Visível como BLOCKED no mesmo monitor no dump.

4. Loop de back-pressure

Reactor ou CompletableFuture configurado para esperar resultado de outro pool que também está cheio. Threads bloqueando umas às outras em cascata — sintoma sutil que só o dump revela.

Como reproduzir em laboratório

Antes de mexer em produção, valide a hipótese em ambiente controlado:

  1. Suba a aplicação com pool reduzido (ex: server.tomcat.threads.max=10) para amplificar o sintoma.
  2. Use k6 ou hey para enviar 50 requisições concorrentes ao endpoint suspeito.
  3. Adicione um Thread.sleep(5000) mockado no serviço externo para simular API lenta.
  4. Tire o dump no exato momento da degradação.

Como extrair o dump na hora do incidente

Quatro caminhos, do mais simples ao mais formal:

# 1) jstack (mais comum, exige JDK no host)
jstack -l 12345 > /tmp/dump-$(date +%s).txt

# 2) jcmd (preferido em JDK 11+, mais portavel)
jcmd 12345 Thread.print -l > /tmp/dump-$(date +%s).txt

# 3) Sinal SIGQUIT (kill -3)
# Escreve no stdout do processo — util quando o app roda em foreground
kill -3 12345

# 4) Spring Boot Actuator (sem precisar SSH no host)
curl -u admin:senha http://localhost:8080/actuator/threaddump > dump.json

Para diagnóstico sério, capture três dumps com 10 segundos de intervalo. Uma thread genuinamente travada aparece na mesma pilha nos três. Uma thread apenas lenta muda de posição na pilha. Essa é a forma mais barata de eliminar falso positivo.

Como interpretar o que você extraiu

Em um dump de Spring Boot, o que você procura primeiro:

  • Threads com prefixo http-nio-*-exec-* em RUNNABLE com pilha em socketRead0 — chamada externa pendurada.
  • Threads http-nio-*-exec-* em BLOCKED esperando o mesmo monitor — contenção interna.
  • Threads HikariCP connection adder ou HikariPool-* agitadas — pool DB sob pressão.
  • Threads scheduling-* em pilhas longas de negócio — scheduler conflitando com requisições.

Exemplo: stuck thread em chamada externa

"http-nio-8080-exec-23" #178 daemon prio=5 os_prio=0
   java.lang.Thread.State: RUNNABLE
   at java.net.SocketInputStream.socketRead0(Native Method)
   at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
   at java.net.SocketInputStream.read(SocketInputStream.java:171)
   at sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:484)
   at org.apache.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:154)
   at org.springframework.http.client.HttpComponentsClientHttpResponse.getBody(...)
   at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:741)
   at com.exemplo.integracao.GatewayClient.consultar(GatewayClient.java:48)
   at com.exemplo.servico.PedidoService.criar(PedidoService.java:91)
   at com.exemplo.controller.PedidoController.post(PedidoController.java:33)

Estado RUNNABLE em socketRead0 é a assinatura clássica de chamada HTTP sem timeout. GatewayClient.consultar está esperando bytes que nunca virão. Tudo no pool de threads do Tomcat vai eventualmente cair nessa armadilha enquanto a API externa estiver mal. Mitigação imediata: configurar timeout no RestTemplate. Mitigação definitiva: circuit breaker (Resilience4j) e degradação graciosa.

Mitigação imediata e correção definitiva

  1. Defina timeouts em todo cliente HTTP. Connect timeout 3-5s, read timeout 10-30s. Nunca, em nenhuma hipótese, deixe nenhum timeout.
  2. Adicione circuit breaker. Resilience4j com failureRateThreshold=50 e waitDurationInOpenState=60s é o padrão do projeto.
  3. Configure pool dedicado por dependência. Spring 6 + Virtual Threads aliviam, mas isolamento por ExecutorService ainda é a melhor forma de uma dependência ruim não derrubar o todo.
  4. Limite a transação. Nada de chamada HTTP dentro de bloco @Transactional — você está segurando conexão do banco.
  5. Habilite o StuckThreadDetectionValve em produção. Threshold de 60s já revela 95% dos casos antes do impacto ficar crítico.

Como o ThreadMine detecta isso

Suba o dump no ThreadMine e o detector de pool exhaustion aponta diretamente os pools saturados: http-nio-8080-exec, HikariPool, scheduling. Para cada thread suspeita, o sistema classifica a causa provável — IO de socket sem timeout, BLOCKED em monitor compartilhado, espera em pool secundário — e mostra exatamente que linha do seu código aparece na pilha.

No fluxo gratuito você analisa um dump e recebe health score, problemas e recomendações. Para separar travamento real de demora ao longo do tempo, suba cada dump e use a Comparação (2 dumps) ou a Timeline (3+ dumps) na sua conta. O resultado é um relatório que você cola no postmortem ou no chamado, sem precisar grifar nada à mão.

Conclusão

Stuck thread em Spring Boot quase sempre tem causa repetitiva: chamada externa sem timeout, transação com IO dentro, contenção desnecessária. Com três dumps e cinco minutos de leitura, você sai do "está lento" para "está lento por causa de X na linha Y". Com a leitura automatizada, o ciclo cai para segundos.

Perguntas frequentes

O que é uma stuck thread no Spring Boot?

Não é uma thread quebrada, e sim uma thread que entrou em uma chamada e não saiu mais. Cada nova requisição toma uma thread nova do pool até não sobrar nenhuma: o endpoint começa rápido, fica lento e para de responder. O servidor não caiu — ficou sem braço livre para atender. Em Tomcat o padrão aparece no log via StuckThreadDetectionValve, com a pilha completa da thread presa além do threshold configurado.

Quais são as causas mais comuns de threads travadas no Spring Boot?

Quatro se repetem: chamada externa sem timeout esgotando o pool HTTP (RestTemplate e WebClient não têm timeout por default); conexão de banco pendurada (query pesada, lock no Postgres, transação órfã ou pool esgotado); sincronização excessiva, com um bloco synchronized segurando o monitor enquanto todas as outras threads se enfileiram; e loop de back-pressure, quando um pool espera resultado de outro pool que também está cheio.

Como capturar um thread dump no momento do incidente?

Quatro caminhos: jstack -l <pid>, jcmd <pid> Thread.print, o sinal SIGQUIT (kill -3, que escreve no stdout do processo) e o endpoint /actuator/threaddump do Spring Boot, sem precisar de SSH no host. Para diagnóstico sério, capture três dumps com 10 segundos de intervalo: a thread genuinamente travada aparece na mesma pilha nos três, enquanto a apenas lenta muda de posição.

O que significa uma thread RUNNABLE em socketRead0 no dump?

É a assinatura clássica de uma chamada HTTP sem timeout: a thread http-nio-*-exec-* está esperando bytes que talvez nunca cheguem da API externa. Já muitas threads em BLOCKED no mesmo monitor indicam contenção interna por sincronização, e threads HikariPool agitadas apontam pressão no pool de banco. É por esses prefixos e estados que você começa a leitura.

Como corrigir stuck threads no Spring Boot?

Mitigação imediata: definir timeouts em todo cliente HTTP (connect 3-5s, read 10-30s), sem exceção. Correção definitiva: circuit breaker com Resilience4j (failureRateThreshold=50, waitDurationInOpenState=60s), pool dedicado por dependência via ExecutorService, nada de chamada HTTP dentro de bloco @Transactional e o StuckThreadDetectionValve habilitado em produção, que com threshold de 60s revela a maioria dos casos antes do impacto ficar crítico.

Quer ver tudo isso aplicado ao seu dump?

Faça upload e tenha health score, problemas detectados e sugestões em segundos. Sem cadastro para o primeiro dump.