Running a Spring Boot application at 80% CPU utilization is a precarious position for any production engineer. While it indicates that you are getting value out of your provisioned hardware, it leaves virtually no headroom for traffic spikes, background tasks, or the inevitable stop-the-world garbage collection cycles. When CPU usage remains consistently high, the application often experiences increased latency, thread exhaustion, and eventual cascading failures. To stabilize the environment, you must move beyond reactive restarts and perform a deep-dive into the application’s runtime behavior, resource allocation, and scaling architecture.
Identifying the Bottlenecks: Metrics and Profiling
Before adjusting any configuration, you must determine whether the high CPU is caused by application logic, garbage collection, or infrastructure overhead. Start by integrating Spring Boot Actuator with Micrometer to export metrics to Prometheus or Datadog. Key metrics to monitor include system.cpu.usage, process.cpu.usage, and jvm.gc.pause. If the process CPU is significantly lower than the system CPU, other processes on the node are competing for resources. If they are nearly identical, the JVM is the primary consumer.
Using Java Flight Recorder (JFR)
For a production-safe, low-overhead profiling session, Java Flight Recorder is indispensable. Execute a short recording during a period of high CPU usage using jcmd <pid> JFR.start duration=60s filename=high_cpu.jfr. Analyze the recording with JDK Mission Control to identify ‘hot’ methods. Look specifically at the ‘Method Profiling’ tab to see which threads are consuming the most CPU cycles. Often, the culprit is inefficient JSON serialization, heavy cryptographic operations, or tight loops in business logic that could be optimized or cached.
JVM and Garbage Collection Tuning
High CPU is frequently a symptom of ‘GC Thrashing,’ where the JVM spends more time reclaiming memory than executing code. For most modern Spring Boot applications, the G1 Garbage Collector (G1GC) is the standard. However, default settings are often insufficient for high-load scenarios. Ensure your heap size is correctly configured by setting -Xms and -Xmx to the same value to prevent the JVM from constantly resizing the heap, which is a CPU-intensive operation.
G1GC Best Practices
When tuning G1GC for high-CPU environments, your primary goal is to reduce the frequency and duration of pauses. Set -XX:MaxGCPauseMillis=200 as a starting point; lowering this too much can increase CPU usage as the collector works harder to meet the target. Additionally, monitor the ‘Humongous Allocations’ in your logs. If your application allocates many large objects, G1GC will struggle, leading to premature Old Generation evacuations. Use -XX:G1HeapRegionSize to adjust the region size if you see frequent humongous allocations. If CPU remains high due to GC, consider increasing the parallel threads with -XX:ParallelGCThreads to match the available cores, ensuring the collector finishes its work as fast as possible.
Thread Pool Optimization
Spring Boot’s default embedded web servers (Tomcat or Undertow) use a thread-per-request model. If your CPU is at 80%, your thread pools might be misconfigured, leading to excessive context switching. If you have too many threads, the CPU spends more time swapping thread contexts than doing actual work. Conversely, too few threads lead to request queuing and high latency.
Tomcat Thread Tuning
In your application.properties, evaluate server.tomcat.threads.max. The default is 200. If your application is CPU-bound (doing heavy computation), this number is likely too high. A better approach for CPU-bound tasks is to keep the thread count closer to the number of available cores. For I/O-bound tasks, a higher number is acceptable, but you must monitor the ‘Runnable’ thread count. If you see hundreds of threads in a ‘Runnable’ state but CPU is pegged, you are facing context-switching overhead. Experiment with reducing the max threads to find the ‘sweet spot’ where throughput peaks before latency degrades.
Database Connection Pool Tuning (HikariCP)
A poorly tuned database connection pool can manifest as high CPU usage in the application layer due to thread contention. HikariCP is the default in Spring Boot and is highly efficient, but it requires precise calibration. A common mistake is setting the maximum-pool-size too high. Each connection requires a thread to manage it, and if the database is slow, those threads stay occupied, leading to the application spinning up more threads to handle incoming requests.
Follow the formula recommended by the HikariCP team: connections = ((core_count * 2) + effective_spindle_count). For a cloud-based environment with SSDs, the spindle count is negligible. If your server has 4 cores, a pool size of 10 is often more performant than a pool of 100. By limiting the pool size, you force the application to queue requests at the connection level rather than overwhelming the CPU with thread management and context switching.
Horizontal vs. Vertical Scaling
When optimization reaches the point of diminishing returns, you must decide between scaling up (vertical) or scaling out (horizontal). Vertical scaling—adding more CPU and RAM to a single instance—is often the quickest fix but has a hard ceiling and does not provide redundancy. It is most effective when the application has a single-threaded bottleneck that benefits from a higher clock speed.
Horizontal scaling—adding more instances of the service—is the preferred approach for production-grade Spring Boot applications, especially in Kubernetes environments. If your CPU is at 80%, your Horizontal Pod Autoscaler (HPA) should ideally be triggered at 60-70% to allow new pods to spin up and pass readiness probes before the existing pods reach a breaking point. Ensure your application is stateless to make horizontal scaling seamless. By distributing the load across more nodes, you reduce the per-node CPU pressure, allowing the JVM more ‘breathing room’ for background tasks and GC.
Real-World Troubleshooting Steps
When an incident occurs and CPU is spiking, follow this checklist: First, check the logs for an unusual volume of errors; exception handling and stack trace generation are surprisingly expensive for the CPU. Second, use top -H -p <pid> to identify specific threads consuming the most resources. If you see VM Thread or GC Task Thread at the top, focus on memory and GC tuning. Third, verify if there is an external factor, such as a sudden increase in traffic or a slow downstream dependency causing request backups.
Managing a high-utilization service requires a balance between aggressive resource usage and system stability. By systematically addressing JVM settings, refining thread and connection pools, and implementing a proactive scaling strategy, you can transform a fragile 80% CPU load into a controlled, high-performance environment. The goal is not just to lower the number, but to ensure that every CPU cycle spent contributes directly to throughput and user experience, rather than being wasted on the friction of an unoptimized runtime. Constant vigilance through metrics and a willingness to iterate on configuration will keep your services resilient as they grow to meet increasing demand.