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 --> L4

Level 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 STARTED LifecycleState. Source: Catalina:type=Context,host=localhost,context=/<app> state attribute, or manager/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 / maxThreads per 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. Default maxThreads is 200 and default minSpareThreads is 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 requestCount on Catalina: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 processingTime in milliseconds divided by the delta of requestCount gives 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. errorCount on 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 CollectionTime divided by wall clock time, from java.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 / MaxFileDescriptorCount from java.lang:type=OperatingSystem, or ls /proc/<pid>/fd | wc -l. Warning sign: above 80% of limit, or monotonic growth unrelated to connection count. Production needs ulimit at 65535 or higher; defaults of 1024 or 4096 are routinely too low.
  • Active session count per context. activeSessions on Catalina: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 StuckThreadDetectionValve configured 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 exposes stuckThreadCount via JMX.
  • JVM CPU utilization. ProcessCpuLoad from java.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 approaching acceptCount (default 100), connections are about to be refused with RST. This is invisible to Tomcat logs.
  • Connection count vs maxConnections. connectionCount / maxConnections on the ThreadPool MBean. Default maxConnections is 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:MaxMetaspaceSize is not set, Metaspace grows until the OS kills the process with no JVM-level OOM.
  • Per-endpoint latency percentiles. Access log %D parsed 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. ThreadCount on java.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 / maxActive and wait count on the pool MBean. Pool exhaustion cascades directly into thread pool exhaustion, because a thread holding an HTTP request and waiting on getConnection() is doubly expensive.
  • 5xx-only error rate from access logs. The JMX errorCount mixes 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 / requestCount is 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: Metaspace after hot redeploys. If MaxMetaspaceSize is 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 both maxConnections and acceptCount are 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 currentThreadsBusy and maxThreads alongside 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, and processingTime are cumulative counters that reset on restart. Netdata handles the reset so throughput and error-rate charts do not flatline or spike after a deploy.