Hook: A high-CPU alert is the kitchen smoke alarm of production: don’t add more chefs first — find the burning pan.
Question: High CPU usage in production.
Answer: In a Spring Boot service, high CPU usually means one or more threads are burning time in a hot loop, expensive parsing, serialization, regex, garbage collection, or lock spinning. First confirm whether the problem is the JVM process, the container, or the node; then use Actuator metrics and a thread dump to identify the hot path. The fix is usually to remove the expensive work, add caching or batching, reduce noisy logging or polling, and only scale out after you know the root cause.
Interview-Ready Answer: “When I see high CPU in production, I first confirm whether the load is on the JVM, the container, or the node. Then I check Spring Boot metrics like process.cpu.usage and take a thread dump to find the hottest stack trace. In practice I want the exact hot method or GC pattern before I change anything, because blindly adding threads or replicas can make CPU and latency worse.”
CPU is the workhorse of the machine. When it is near 100% for a sustained period, the app is spending too much time doing compute work, not waiting on network or disk. In Linux, 100% usually means one full core, so a Java process can show 300% on a 4-core box and still be normal if it is truly using three cores. In Kubernetes, also check whether the pod is being throttled (CPU limited by cgroups, the Linux container quota system), because a pod can be slowed down even if the node still has free cores.
process.cpu.usage and system.cpu.usage. These are ratios from 0.0 to 1.0, not percentages. Add jvm.threads.live, jvm.gc.pause, and request latency metrics so you can tell CPU pressure from garbage collection or traffic spikes.jcmd <pid> Thread.print, /actuator/threaddump, or kill -3. A thread dump is a snapshot of every thread right now. Look for many RUNNABLE threads with the same stack trace. RUNNABLE means the thread is able to run or is running; it does not mean “blocked”.| Signal | Likely meaning | First move |
|---|---|---|
| Hot RUNNABLE stack | CPU-bound code | Profile the method |
| High GC time | Allocation churn | Check GC logs |
| BLOCKED / WAITING | Lock or I/O issue | Inspect locks and downstreams |
| Pod throttling | CPU limit hit | Raise limit or reduce bursts |
| Tool | Best for | Cost |
|---|---|---|
top / htop | First glance | Very low |
| Thread dump | Hot stack snapshot | Very low |
| JFR | Time-based profiling | Low |
| async-profiler | Hot methods | Low |
Practical interview detail: if a Tomcat app is CPU-heavy, remember the default max worker thread count is commonly 200. Adding even more threads can increase context switching, which is the overhead of the OS constantly swapping work between threads, and that can make CPU usage worse instead of better.
Real-World Story: A checkout service in a Spring Boot e-commerce app started hitting 90% CPU during a flash sale. Users saw slow payment confirmation, p95 latency jumped from 120 ms to 3–4 seconds, and Kubernetes restarted pods because the readiness probe timed out. The logs were mostly clean, which made the problem feel mysterious until a thread dump showed many request threads stuck in a regex-based coupon parser. The fix was to precompile the pattern, cache repeated lookups, and reduce the amount of per-request logging. After that, CPU dropped, response time stabilized, and the same pod count handled the sale comfortably.
What went wrong in the outage was not “Spring Boot is slow”; it was a small piece of expensive code running for every request. That is the key lesson: production CPU issues are usually a hot path problem, not a framework problem.
package com.example.highcpu;\n\nimport java.lang.management.ManagementFactory;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\n\nimport com.sun.management.OperatingSystemMXBean;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.scheduling.annotation.EnableScheduling;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Component;\nimport org.springframework.stereotype.Service;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ResponseStatusException;\n\n@SpringBootApplication\n@EnableScheduling\npublic class HighCpuApplication {\n public static void main(String[] args) {\n SpringApplication.run(HighCpuApplication.class, args);\n }\n}\n\n@RestController\n@RequestMapping("/api")\nclass CpuController {\n private final PrimeService primeService;\n private final CpuMonitor cpuMonitor;\n\n CpuController(PrimeService primeService, CpuMonitor cpuMonitor) {\n this.primeService = primeService;\n this.cpuMonitor = cpuMonitor;\n }\n\n @GetMapping("/cpu")\n public Map<String, Object> cpu() {\n // This endpoint is a tiny in-app signal; real production observability should still use Actuator and metrics.\n return cpuMonitor.snapshot();\n }\n\n @GetMapping("/primes/slow")\n public Map<String, Object> slow(@RequestParam int n) {\n return primeService.solve(n, false);\n }\n\n @GetMapping("/primes/cached")\n public Map<String, Object> cached(@RequestParam int n) {\n return primeService.solve(n, true);\n }\n}\n\n@Service\nclass PrimeService {\n private final ConcurrentMap<Integer, Integer> cache = new ConcurrentHashMap<>();\n\n public Map<String, Object> solve(int n, boolean useCache) {\n validate(n);\n long start = System.nanoTime();\n int prime = useCache ? cache.computeIfAbsent(n, this::nthPrime) : nthPrime(n);\n long tookMs = (System.nanoTime() - start) / 1_000_000;\n\n Map<String, Object> result = new LinkedHashMap<>();\n result.put("n", n);\n result.put("prime", prime);\n result.put("cached", useCache);\n result.put("tookMs", tookMs);\n return result;\n }\n\n private void validate(int n) {\n if (n <= 0) {\n throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "n must be positive");\n }\n // A guardrail is important in production: bad input should fail fast instead of burning CPU forever.\n if (n > 10_000) {\n throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "n is too large for this demo");\n }\n }\n\n private int nthPrime(int n) {\n int count = 0;\n int candidate = 1;\n while (count < n) {\n candidate++;\n if (isPrime(candidate)) {\n count++;\n }\n }\n return candidate;\n }\n\n private boolean isPrime(int number) {\n if (number < 2) return false;\n if (number == 2) return true;\n if (number % 2 == 0) return false;\n\n int limit = (int) Math.sqrt(number);\n for (int i = 3; i <= limit; i += 2) {\n if (number % i == 0) {\n return false;\n }\n }\n return true;\n }\n}\n\n@Component\nclass CpuMonitor {\n private static final Logger log = LoggerFactory.getLogger(CpuMonitor.class);\n private final OperatingSystemMXBean osMxBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);\n\n @Scheduled(fixedDelay = 5000)\n public void logCpu() {\n if (osMxBean == null) {\n log.warn("CPU metrics are not available on this JVM");\n return;\n }\n\n double processLoad = osMxBean.getProcessCpuLoad();\n double systemLoad = osMxBean.getSystemCpuLoad();\n\n if (processLoad < 0 || systemLoad < 0) {\n log.info("CPU metrics are temporarily unavailable");\n return;\n }\n\n int cores = Runtime.getRuntime().availableProcessors();\n log.info("CPU monitor: process={}%, system={}%, cores={}", round(processLoad * 100), round(systemLoad * 100), cores);\n\n // The warning points you to the next debugging step; it is not a fix by itself.\n if (processLoad > 0.75) {\n log.warn("High process CPU detected. Capture a thread dump and profile the hot path.");\n }\n }\n\n public Map<String, Object> snapshot() {\n Map<String, Object> result = new LinkedHashMap<>();\n result.put("processCpuPercent", osMxBean == null ? "unavailable" : roundSafe(osMxBean.getProcessCpuLoad()));\n result.put("systemCpuPercent", osMxBean == null ? "unavailable" : roundSafe(osMxBean.getSystemCpuLoad()));\n result.put("cores", Runtime.getRuntime().availableProcessors());\n return result;\n }\n\n private long round(double value) {\n return Math.round(value);\n }\n\n private Object roundSafe(double value) {\n if (value < 0) {\n return "unavailable";\n }\n return round(value * 100);\n }\n}Follow-up & Tricky Questions:
RUNNABLE threads; GC thrash shows frequent garbage collection activity, allocation pressure, and pause logs. JFR or GC logs confirm it quickly.Thread.sleep a fix? No. Sleeping only hides the symptom. It lowers current CPU for that thread, but it does not remove the expensive work and often increases user-facing latency.Common Mistakes:
Memory Hook: Think: measure, isolate, fix. A hot CPU is a smoke alarm, not the fire itself; your job is to find the exact pan on the stove before you open more burners.
Cheat Sheet:
process.cpu.usage and system.cpu.usage.RUNNABLE stacks.Practice Tasks:
/api/primes/slow?n=5000 with /api/primes/cached?n=5000.jcmd <pid> Thread.print and identify the busy stack.process.cpu.usage while you generate load with a tool like wrk or ab.