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
Back

Guia técnico

Spring Boot stuck threads: how do you diagnose them?

A Tomcat pool that runs dry, requests that never return, latency that only climbs. The fast path from the symptom to the root cause with a thread dump.

Try it free

In Spring Boot products, a "stuck thread" rarely means a broken thread. It means a thread that entered a call and never left — and every new request takes a fresh thread from the pool, until the point where none are left. The classic symptom is familiar: the endpoint you are testing starts fast, then gets slow, then stops responding entirely. The server did not crash; it simply ran out of a free arm to answer with.

This pattern has a name in virtually every modern Java container — Tomcat calls it a stuck thread, WebLogic calls it a hogging thread, JBoss calls it a blocked thread. In Spring Boot, which normally runs on Tomcat or Undertow, the tag in the log is org.apache.catalina.valves.StuckThreadDetectionValve. When it is active, this valve emits a WARN with the full stack of the thread that has been stuck for longer than threshold seconds.

This guide shows how to draw a conclusion from its log and from the thread dump — no guessing, in a repeatable sequence.

The symptom of a stuck thread

In Tomcat, the most explicit evidence shows up in the application log:

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.

Five threads in the pool have already crossed the 60-second limit. If the pool has 200 threads (the Tomcat default), you still have headroom; if it drops to 20, it is over. The P99 latency will deteriorate before the incident becomes visible to the customer.

The four most common causes

1. HTTP pool exhausted by an external call with no timeout

By default, Spring Boot sets no timeout on RestTemplate or WebClient unless you configure one explicitly. An external API that answers in 200ms most of the time and in 2 minutes under load will generate stuck threads immediately.

2. A hung database connection

A heavy query, a lock in Postgres, an orphaned transaction, or an exhausted DBCP pool. The thread sits in socketRead0 on the JDBC stack, unresponsive to interruption.

3. Excessive synchronization

A synchronized block around IO or a long stretch of work. One thread holds the monitor for seconds and all the others queue up behind it. Visible as BLOCKED on the same monitor in the dump.

4. A back-pressure loop

A Reactor or CompletableFuture pipeline configured to wait for the result of another pool that is also full. Threads blocking one another in a cascade — a subtle symptom that only the dump reveals.

How to reproduce it in a lab

Before touching production, validate the hypothesis in a controlled environment:

  1. Start the application with a reduced pool (e.g. server.tomcat.threads.max=10) to amplify the symptom.
  2. Use k6 or hey to send 50 concurrent requests to the suspect endpoint.
  3. Add a mocked Thread.sleep(5000) in the external service to simulate a slow API.
  4. Take the dump at the exact moment of the degradation.

How to grab the dump during the incident

Four paths, from the simplest to the most formal:

# 1) jstack (most common, requires a JDK on the host)
jstack -l 12345 > /tmp/dump-$(date +%s).txt

# 2) jcmd (preferred on JDK 11+, more portable)
jcmd 12345 Thread.print -l > /tmp/dump-$(date +%s).txt

# 3) SIGQUIT signal (kill -3)
# Writes to the process stdout — useful when the app runs in the foreground
kill -3 12345

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

For a serious diagnosis, capture three dumps 10 seconds apart. A genuinely stuck thread appears on the same stack in all three. A merely slow thread moves position in the stack. This is the cheapest way to rule out a false positive.

How to interpret what you captured

In a Spring Boot dump, here is what you look for first:

  • Threads with the http-nio-*-exec-* prefix in RUNNABLE with a stack in socketRead0 — an external call left hanging.
  • http-nio-*-exec-* threads in BLOCKED waiting on the same monitor — internal contention.
  • HikariCP connection adder or HikariPool-* threads churning — the DB pool under pressure.
  • scheduling-* threads deep in business logic stacks — a scheduler colliding with requests.

Example: a stuck thread in an external call

"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)

A RUNNABLE state in socketRead0 is the classic signature of an HTTP call with no timeout. GatewayClient.consultar is waiting for bytes that will never come. Everything in the Tomcat thread pool will eventually fall into this trap while the external API is unhealthy. Immediate mitigation: configure a timeout on the RestTemplate. Permanent mitigation: a circuit breaker (Resilience4j) and graceful degradation.

Immediate mitigation and permanent fix

  1. Set timeouts on every HTTP client. Connect timeout 3-5s, read timeout 10-30s. Never, under any circumstances, leave no timeout.
  2. Add a circuit breaker. Resilience4j with failureRateThreshold=50 and waitDurationInOpenState=60s is the project standard.
  3. Configure a dedicated pool per dependency. Spring 6 + Virtual Threads help, but isolation via an ExecutorService is still the best way to keep one bad dependency from bringing down the whole.
  4. Keep the transaction tight. No HTTP call inside a @Transactional block — you are holding a database connection.
  5. Enable the StuckThreadDetectionValve in production. A 60s threshold already reveals 95% of cases before the impact turns critical.

How ThreadMine detects this

Upload the dump to ThreadMine and the pool exhaustion detector points straight at the saturated pools: http-nio-8080-exec, HikariPool, scheduling. For each suspect thread, the system classifies the likely cause — socket IO with no timeout, BLOCKED on a shared monitor, waiting on a secondary pool — and shows exactly which line of your code appears in the stack.

On the free flow you analyze one dump and get a health score, problems, and recommendations. To separate a real hang from slowness over time, upload each dump and use the Comparison (2 dumps) or the Timeline (3+ dumps) in your account. The result is a report you can paste into a postmortem or a ticket, with nothing to highlight by hand.

Conclusion

Stuck threads in Spring Boot almost always have a repeatable cause: an external call with no timeout, a transaction with IO inside it, unnecessary contention. With three dumps and five minutes of reading, you go from "it is slow" to "it is slow because of X on line Y." With automated reading, the cycle drops to seconds.

Frequently asked questions

What is a stuck thread in Spring Boot?

It is not a broken thread but a thread that entered a call and never left. Every new request takes a fresh thread from the pool until none are left: the endpoint starts fast, gets slow, then stops responding. The server did not crash — it simply ran out of a free arm to answer with. In Tomcat the pattern shows up in the log through the StuckThreadDetectionValve, with the full stack of the thread stuck beyond the configured threshold.

What are the most common causes of stuck threads in Spring Boot?

Four recur: an external call with no timeout exhausting the HTTP pool (RestTemplate and WebClient have no timeout by default); a hung database connection (a heavy query, a Postgres lock, an orphaned transaction, or an exhausted pool); excessive synchronization, where a synchronized block holds the monitor while every other thread queues up behind it; and a back-pressure loop, when one pool waits on the result of another pool that is also full.

How do I capture a thread dump during the incident?

Four paths: jstack -l <pid>, jcmd <pid> Thread.print, the SIGQUIT signal (kill -3, which writes to the process stdout), and Spring Boot’s /actuator/threaddump endpoint, with no SSH into the host needed. For a serious diagnosis, capture three dumps 10 seconds apart: a genuinely stuck thread appears on the same stack in all three, while a merely slow one moves position.

What does a RUNNABLE thread in socketRead0 mean in the dump?

It is the classic signature of an HTTP call with no timeout: the http-nio-*-exec-* thread is waiting for bytes that may never arrive from the external API. Many threads BLOCKED on the same monitor instead point to internal contention from synchronization, and churning HikariPool threads point to pressure on the database pool. Those prefixes and states are where you start reading.

How do I fix stuck threads in Spring Boot?

Immediate mitigation: set timeouts on every HTTP client (connect 3-5s, read 10-30s), with no exceptions. Permanent fix: a circuit breaker with Resilience4j (failureRateThreshold=50, waitDurationInOpenState=60s), a dedicated pool per dependency via an ExecutorService, no HTTP call inside a @Transactional block, and the StuckThreadDetectionValve enabled in production, which with a 60s threshold reveals most cases before the impact turns critical.

Want to see all this applied to your own dump?

Upload a dump and get a health score, detected problems and fixes in seconds. No signup for the first dump.