Tomcat’s capacity model rests on a small number of bounded resources: a worker thread pool, a connection poller, the JVM heap, Metaspace, and the OS file descriptor table. Most production outages are one of these hitting its limit while the JVM process keeps running. A monitoring setup that only tracks CPU and memory will miss the dominant failure mode: thread pool exhaustion with the JVM at low CPU and a healthy process.
This checklist is organized into four cumulative maturity levels: survival, operational, mature, and expert. Each level assumes the previous one is in place. The single most important Tomcat-specific signal, thread pool utilization, sits at survival level because without it you cannot distinguish “Tomcat is up” from “Tomcat is accepting connections but cannot process them.”
The signals come from three sources: the Tomcat Manager status XML at /manager/status?XML=true, JMX MBeans under the Catalina: and java.lang: domains, and OS-level tools such as ss and /proc. Several critical signals, including active session count, GC activity, and file descriptor usage, are not exposed through Manager XML and require JMX. Plan for JMX from the start.
Read your current dashboards against each level. Anything missing is a gap. The “Common blind spots” section lists the gaps that most reliably produce 3 a.m. pages.
How the levels are organized
The four levels build upward. Survival tells you the service is alive. Operational tells you it is fast enough. Mature tells you it is not about to saturate. Expert gives you leading indicators for leaks and queues before users notice.
flowchart TD
L4[Level 4 expert
accept queue, post-GC trend, metaspace leak]
L3[Level 3 mature
sessions, GC ratio, fd used/limit, stuck threads]
L2[Level 2 operational
throughput, processing time, error split, memory pools]
L1[Level 1 survival
process alive, connector responds, context STARTED, thread pool]
L1 --> L2 --> L3 --> L4Level 1: survival
The absolute minimum to know Tomcat is alive and serving. Without all four of these, you are flying blind.
- JVM process alive. The process exists. Source: OS process table,
pgrep -f 'org.apache.catalina.startup.Bootstrap'for standalone, or the application JAR for embedded. Why it matters: catches JVM crashes and OS OOM kills. Limitation: a live process does not mean requests are being served. The OS OOM killer leaves no Tomcat-level log, only the kernel log. - Connector responds to a real request. A TCP connect is not enough. You must issue an HTTP request that exercises the servlet pipeline. Why it matters: a TCP connect can succeed while the accept queue holds the connection and no worker thread picks it up. A 503 returned by Tomcat itself signals thread pool or connector saturation, not an application error.
- Application context is STARTED. Each deployed Context must be in the
STARTEDLifecycleState. Source:Catalina:type=Context,host=localhost,context=/<app>stateattribute, ormanager/text/list. Why it matters: a FAILED context returns 404 to every request, which looks like a missing route rather than a down app. Tomcat does not restart failed contexts automatically. - Thread pool utilization.
currentThreadsBusy / maxThreadsper connector. Source:Catalina:type=ThreadPool,name="http-nio-8080". Why this is survival-level: when busy reaches maxThreads, Tomcat stops processing new requests even though the JVM is healthy and the port is open. DefaultmaxThreadsis 200 and defaultminSpareThreadsis 10.
Threshold guidance for the thread pool: sustained busy at 100% of maxThreads (with maxThreads > 50 and uptime greater than 120 seconds) is a page. Sustained ratio above 0.80 for more than five minutes is a ticket. Gate alerts on uptime to avoid cold-start spikes, because the pool starts at minSpareThreads and the first burst can briefly saturate before more threads are created.
Level 2: operational
What a competent team monitors in addition to survival. These turn “Tomcat is slow” into a diagnosable signal.
- Request throughput. Rate derived from cumulative
requestCountonCatalina:type=GlobalRequestProcessor,name="http-nio-8080". Warning sign: throughput drops below 50% of same-daypart baseline while upstream reports traffic arriving. The counter resets on restart, so compute deltas with reset awareness. - Request processing time. Cumulative
processingTimein milliseconds divided by the delta ofrequestCountgives an average. Warning sign: average trending upward more than 2x over baseline. Important limitation: JMX gives averages only. For percentiles you need the access log with%D(request duration in milliseconds) configured, which is not present in the default or combined log pattern. - Error count, split by class.
errorCounton the GlobalRequestProcessor MBean lumps 4xx and 5xx together and cannot safely be paged on, because crawler 404s would fire the alert. For 5xx-only alerting, parse the access log by status code. Warning sign: any sustained 503 from Tomcat itself means thread or connector exhaustion, not an application bug. - JVM memory pools. Per-pool breakdown from
java.lang:type=MemoryPool,name=.... Old Gen post-GC rising indicates a memory leak. Metaspace growing monotonically across redeploys indicates a classloader leak. Pool names vary by GC algorithm: “G1 Old Gen”, “PS Old Gen”, “Tenured Gen”.
Level 3: mature
Full coverage for a production-grade deployment. These signals catch saturation and internal state before users do.
- Post-GC heap baseline. Not instantaneous usage. The sawtooth fills then drops on GC, so high instantaneous usage is normal. What matters is the valley after GC. Warning sign: post-GC old gen consistently above 85% of max and trending up. Alerting on raw heap above 80% produces constant false positives.
- GC time ratio. Cumulative
CollectionTimedivided by wall clock time, fromjava.lang:type=GarbageCollector,name=.... Healthy is below 5%. Above 10% is concerning. Above 20% is a GC death spiral. With G1GC (default since JDK 9), any Full GC warrants investigation. - Open file descriptors vs limit.
OpenFileDescriptorCount/MaxFileDescriptorCountfromjava.lang:type=OperatingSystem, orls /proc/<pid>/fd | wc -l. Warning sign: above 80% of limit, or monotonic growth unrelated to connection count. Production needsulimitat 65535 or higher; defaults of 1024 or 4096 are routinely too low. - Active session count per context.
activeSessionsonCatalina:type=Manager,host=localhost,context=/<app>. Not available via Manager XML, requires JMX. Warning sign: monotonic growth without plateau. Sessions persist until timeout (default 30 minutes), so a traffic drop does not immediately reduce the count. Bots that do not send cookies can create one session per request. - Stuck thread detection. Requires
StuckThreadDetectionValveconfigured explicitly. It is not enabled by default and its default threshold is 600 seconds. Without it, stuck threads are indistinguishable from a normal backend slowdown in JMX metrics. The valve exposesstuckThreadCountvia JMX. - JVM CPU utilization.
ProcessCpuLoadfromjava.lang:type=OperatingSystem. If more than 30% of CPU is GC, you have a memory problem, not a CPU problem. Exclude cold start from alerting, since JIT compilation consumes CPU for the first few minutes after startup.
Level 4: expert
Leading indicators and leak detection. These are the signals that prevent the next major incident.
- Accept queue depth.
ss -tnl 'sport = :8080'Recv-Q. No JMX counter exists for this. When Recv-Q is non-zero and approachingacceptCount(default 100), connections are about to be refused with RST. This is invisible to Tomcat logs. - Connection count vs maxConnections.
connectionCount/maxConnectionson the ThreadPool MBean. DefaultmaxConnectionsis 10000 for NIO/NIO2 (8192 for APR/native). Idle keepalive connections consume poller slots and file descriptors but not threads, so connection count can legitimately far exceed busy threads. - Post-GC heap trend over days. Extrapolate the rising valley to project when old gen hits 90% of max. GC overhead feedback often makes the cliff arrive sooner than a linear projection suggests.
- Metaspace growth per redeploy. Measure the step increase after each undeploy and redeploy cycle. If Metaspace does not return to within 10% of its pre-deploy value, you have a classloader leak. If
-XX:MaxMetaspaceSizeis not set, Metaspace grows until the OS kills the process with no JVM-level OOM. - Per-endpoint latency percentiles. Access log
%Dparsed for p95 and p99. A p50 of 100 ms with a p99 of 15 seconds averages to “fine” while 1% of users time out. Bimodal distributions indicate two distinct failure modes. - Total JVM thread count.
ThreadCountonjava.lang:type=Threading. Expected baseline is roughly maxThreads plus 50 overhead threads. Each thread consumes stack memory (default 512 KB to 1 MB via-Xss), so 500 threads silently commits around 500 MB off-heap. - JDBC connection pool utilization.
numActive/maxActiveand wait count on the pool MBean. Pool exhaustion cascades directly into thread pool exhaustion, because a thread holding an HTTP request and waiting ongetConnection()is doubly expensive. - 5xx-only error rate from access logs. The JMX
errorCountmixes 4xx and 5xx and cannot be paged on safely. Log parsing by status code is the only reliable source for server-error alerting.
Common blind spots
These are the gaps that most reliably produce incidents, drawn from the failure patterns that recur across Tomcat deployments.
- Monitoring the PID, not the thread pool. JVM alive does not mean Tomcat is serving. A process check alone misses thread pool exhaustion, the most common Tomcat outage.
- Alerting on instantaneous heap. The sawtooth is supposed to fill before GC runs. Only the post-GC valley indicates a leak.
- Relying on average latency. JMX
processingTime / requestCountis an average. Stuck threads averaged with fast requests look healthy while users time out. Percentiles require access log%D. - Missing the accept queue. When threads and connections are both exhausted, requests queue in the OS TCP backlog and are then refused with RST. No JMX counter covers this.
- Default access log omits timing. The default and combined patterns lack
%D. Without it, per-request latency analysis is impossible retroactively. - Ignoring Metaspace. Teams watch heap and are blindsided by
OutOfMemoryError: Metaspaceafter hot redeploys. IfMaxMetaspaceSizeis unset, the process dies from OS OOM kill with no JVM error. - Confusing thread exhaustion with connection exhaustion. With NIO, the poller keeps accepting connections up to
maxConnections(10000 default) even when the thread pool is full. Connections are only refused when bothmaxConnectionsandacceptCountare exceeded. - Leaving the Manager app exposed. Default installations include the Manager and Host Manager apps, which allow WAR upload and remote code execution. Remove them or restrict to localhost in production.
How Netdata helps
Netdata’s per-second collection is useful for Tomcat specifically because the dominant failure modes are saturation cascades that develop over tens of seconds, not minutes. The value is in correlating signals on a shared timeline.
- Thread pool saturation correlation. Netdata surfaces
currentThreadsBusyandmaxThreadsalongside request throughput and processing time on the same timeline, so you can see whether a busy pool is caused by slow requests (throughput dropping, processing time rising) or a pure load spike (throughput rising). - Post-GC heap versus instantaneous heap. Correlating heap utilization with GC collection time makes the sawtooth valley visible without manual chart inspection, and the valley is what actually indicates a leak.
- GC overhead ratio. Collection time as a fraction of wall clock time, plotted next to request latency, shows whether latency spikes align with GC pauses rather than application code.
- File descriptor pressure. FD count plotted against connection count separates a connection surge from an FD leak, which require different responses.
- Counter-reset-aware rate computation.
requestCount,errorCount, andprocessingTimeare cumulative counters that reset on restart. Netdata handles the reset so throughput and error-rate charts do not flatline or spike after a deploy.
Related guides
- How Tomcat actually works in production: a mental model for operators
- Tomcat monitoring maturity model: from survival to expert
- Tomcat process not running: crashes, OOM-kills, and failed restarts
- Tomcat accepts connections but never responds: the TCP-connect trap
- Tomcat java.net.BindException: Address already in use: the connector never starts
- Tomcat thread pool exhaustion: currentThreadsBusy at maxThreads and requests hanging
- Tomcat HTTP Status 503 Service Unavailable: the connector is out of threads
- Tomcat maxThreads and minSpareThreads: sizing the executor correctly
- Tomcat threads busy but CPU idle: telling a blocked backend from a GC spiral
- Tomcat virtual threads (JDK 21+): why currentThreadsBusy reports -1
- Tomcat java.lang.OutOfMemoryError: Java heap space: the heap is genuinely full
- Tomcat OutOfMemoryError: GC overhead limit exceeded: GC running but freeing nothing






