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

Technical guide

Tomcat thread dump: how to capture and analyze it

Where the Tomcat PID hides, why kill -3 lands in catalina.out, what the http-nio exec pools mean, and how to spot a maxThreads exhaustion before your users do.

Try it free

Tomcat hangs have a signature: the port still accepts connections, the process is alive, CPU looks calm — and every request sits there until the client gives up. The tool that turns that mystery into a diagnosis is a thread dump. But Tomcat adds its own wrinkles to the standard procedure: the JVM process is called Bootstrap, not tomcat; the classic kill -3 writes the dump somewhere people don’t expect; and the dump itself is dominated by connector pools with names like http-nio-8080-exec-42 that you need to know how to read. This guide covers the Tomcat-specific path end to end.

Finding the Tomcat PID

Tomcat’s JVM runs the class org.apache.catalina.startup.Bootstrap, so a naive ps aux | grep tomcat can miss it (or match your grep). The two reliable options:

# jps ships with the JDK and lists JVMs by main class
$ jps -l
41317 org.apache.catalina.startup.Bootstrap
41551 jdk.jcmd/sun.tools.jps.Jps

# or match the catalina system properties every Tomcat carries
$ pgrep -f catalina
41317

If several Tomcat instances run on the same host, tell them apart by -Dcatalina.base in the full command line (ps -fp 41317) — each instance points to its own base directory.

Capturing with jstack or jcmd

With the PID in hand, the capture is the standard JDK procedure — with one Tomcat-specific catch: the attach mechanism requires running as the same user as the target JVM. Production Tomcat usually runs as a service user (tomcat), so prefix accordingly:

# as the tomcat user (attach requires same user as the JVM)
$ sudo -u tomcat jstack -l 41317 > /tmp/tomcat-dump-1.txt

# jcmd is the modern equivalent
$ sudo -u tomcat jcmd 41317 Thread.print -l > /tmp/tomcat-dump-1.txt

Take three dumps a few seconds apart — a hang diagnosis is about what doesn’t move between snapshots. If you need the full menu of capture methods (Windows service, Spring Boot Actuator, no-JDK hosts), the general guide to taking a thread dump covers each one; everything there applies to Tomcat too.

kill -3: the dump lands in catalina.out

The oldest trick still works everywhere, including hosts without a JDK: kill -3 (SIGQUIT) asks the JVM to print a thread dump. The JVM handles the signal itself — Tomcat does not stop, restart or drop a single request. The catch is where the dump goes: to the JVM’s stdout, which Tomcat’s startup scripts redirect to CATALINA_BASE/logs/catalina.out.

$ kill -3 41317          # nothing appears on your terminal - that's expected
$ tail -n 400 $CATALINA_BASE/logs/catalina.out
Full thread dump OpenJDK 64-Bit Server VM (21.0.3+7 mixed mode):
"http-nio-8080-exec-1" #52 daemon prio=5 ...
  • Nothing shows up in catalina.out? On systemd installs that don’t use the catalina.sh redirect, stdout goes to the journal: journalctl -u tomcat -n 400.
  • Repeat it. Each kill -3 appends another full dump to the same file — send the signal two or three times, seconds apart, and you get the multi-snapshot series in one place.
  • Mind the rotation. catalina.out is often rotated or truncated by logrotate; grab the dump excerpt out of it before it disappears.

Tomcat in containers

In Docker/Kubernetes the same two paths exist, chosen by what the image ships:

# JDK image (jcmd available): Tomcat is usually PID 1
$ docker exec my-tomcat jcmd 1 Thread.print -l > tomcat-dump.txt

# JRE-only image (no jcmd/jstack): the JVM's signal handler still works
$ docker exec my-tomcat kill -3 1
$ docker logs --tail 400 my-tomcat   # stdout of PID 1 = docker logs

The kill -3 route is the reason a JRE-only image is not a dead end: the dump goes to the container’s stdout, which is exactly what docker logs (or kubectl logs) shows. In Kubernetes, kubectl exec <pod> -- kill -3 1 followed by kubectl logs <pod> is the equivalent.

Reading the dump: Tomcat’s thread pools

A Tomcat dump is dominated by a few thread families, and the names encode connector, port and role:

  • http-nio-8080-exec-N — the worker pool of the HTTP connector on port 8080. These threads run your servlets, filters and controllers; they are where your application’s problems show up. Their count is capped by maxThreads (default 200 per connector). A TLS connector shows up as https-jsse-nio-8443-exec-N, AJP as ajp-nio-8009-exec-N, and if you configured a shared <Executor>, the name is the executor’s (typically catalina-exec-N).
  • http-nio-8080-Acceptor — accepts new TCP connections and hands them to the poller. One per connector; almost always RUNNABLE in accept. Benign.
  • http-nio-8080-Poller — the NIO selector loop watching sockets for readable data. RUNNABLE in select is its idle state. Benign.
  • catalina-utility-N — Tomcat’s internal housekeeping (session expiration, deployment scanning). Usually WAITING; rarely the problem.

The reading rule: ignore the plumbing (Acceptor, Poller, utility) and read the exec pool. An idle exec worker parks in WAITING on the pool’s queue — healthy. A busy one is RUNNABLE inside your application code or blocked on something. What the states mean precisely — and why RUNNABLE on a socket read is not “busy CPU” — is covered in the thread states field guide.

Classic symptoms: maxThreads exhausted, requests queueing

All exec threads busy on the same downstream

"http-nio-8080-exec-17" #71 daemon prio=5 ... runnable
   java.lang.Thread.State: RUNNABLE
	at sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:191)
	at java.net.Socket$SocketInputStream.read(Socket.java:1099)
	at com.mysql.cj.protocol.ReadAheadInputStream.fill(...)
	at com.example.OrderRepository.findPending(OrderRepository.java:44)

Two hundred exec threads with this shape means the entire pool is parked inside socket reads toward the database (or another downstream). The JVM reports RUNNABLE because the wait is native, but nobody is doing work — and with the pool at maxThreads, new requests can’t get a worker. They wait in the connector’s accept queue (bounded by acceptCount, default 100) until something frees up or the client times out. From the outside: “Tomcat is up but nothing responds.” Raising maxThreads does not fix this — it adds more threads to park on the same slow downstream.

Exec threads BLOCKED on one monitor

"http-nio-8080-exec-9" #63 daemon prio=5 ... waiting for monitor entry
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.example.LegacyCache.get(LegacyCache.java:71)
	- waiting to lock <0x000000076ab61c58> (a com.example.LegacyCache)

Dozens of exec threads waiting to lock the same address is a contention hotspot: one synchronized section serializing the whole connector pool. Find the thread that reports locked <that address> — whatever it is doing (often slow IO inside the critical section) is the real bottleneck. If the holder is itself waiting on one of the blocked threads, you have a deadlock, and the JVM prints it explicitly at the bottom of the dump.

The healthy baseline

For contrast: on a quiet server, most exec threads sit in WAITING on the pool queue (TaskQueue / parkNanos), a handful are RUNNABLE actually serving requests, and the pool count is well under maxThreads. That picture is what “nothing is wrong” looks like — knowing it is how you recognize the two patterns above at a glance.

Automatic analysis of a Tomcat dump

Everything above is doable by hand — at 3 a.m., with 200 exec threads and an incident channel pinging, it is also easy to get wrong. That reading is what ThreadMine automates: paste the catalina.out excerpt or the jstack output into the free online Java thread dump analyzer and it parses the pools by name, separates idle workers from busy ones, and runs a set of detectors over the result — pool exhaustion (exec count pinned at the cap, with the evidence), contention hotspots resolved to the lock holder, deadlocks, and thread leaks across snapshots.

The output is ranked findings with severity and an A–F health score instead of a wall of stack traces — the difference between “here are your 200 threads” and “the http-nio-8080 pool is exhausted and 187 of its workers are parked inside the same JDBC call.” Upload two or three dumps from the same incident and the timeline shows when the pool started saturating, not just that it did.

Conclusion

A Tomcat thread dump is the standard JVM procedure plus local knowledge: the PID hides behind Bootstrap, kill -3 lands in catalina.out (or docker logs), and the story is almost always in the http-nio-*-exec pool — exhausted against a slow downstream, serialized on a hot lock, or healthy and idle. Capture three dumps a few seconds apart, read the exec pool against the healthy baseline, and let a tool do the grouping when the thread count is in the hundreds.

Frequently asked questions

Does kill -3 stop or restart Tomcat?

No. SIGQUIT is intercepted by the JVM’s signal handler, which prints the thread dump to stdout and keeps running. Tomcat does not stop, restart or drop requests — kill -3 is safe to run against a production instance, which is exactly why it is the classic capture method for Tomcat.

Where does the thread dump go when I use kill -3 on Tomcat?

To Tomcat’s stdout, which the standard startup scripts redirect to CATALINA_BASE/logs/catalina.out — not to a separate file and not to your terminal. On a systemd service without that redirect, check journalctl -u tomcat; in a container, it appears in docker logs.

How many thread dumps should I take to diagnose a Tomcat hang?

At least three, a few seconds apart. One dump shows a snapshot; the comparison shows movement. Exec threads that change stacks between dumps are slow but alive; threads frozen on the same frame and lock in every dump are the ones actually stuck, and a pool that is pinned at maxThreads across all three is exhausted rather than momentarily busy.

Is ThreadMine free to analyze a Tomcat thread dump?

The first dump does not even ask for a signup: paste or upload the catalina.out excerpt or the jstack output and you get the full diagnosis. The Free plan adds daily analyses with history; paid plans start at US$ 9/month (Pro).

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.