How We Reduced Kubernetes Resource Usage Through Smarter Resource Tuning
A practical guide to right-sizing requests and tuning CPU and memory limits for JVM workloads
How We Reduced Kubernetes Resource Usage Through Smarter Resource Tuning
A practical guide to right-sizing requests and tuning CPU and memory limits for JVM workloads
For anyone exploring this topic but unable to read it, here’s a **free link** to read and revisit anytime.

How CPU and memory limits shape resource usage across a Kubernetes cluster
Most Kubernetes clusters don’t fail because they completely run out of CPU or memory. More often, they become inefficient because pods request far more resources than they actually use, scale inefficiently, or experience CPU throttling and memory pressure under load. Over time, these issues lead to higher infrastructure costs and inconsistent application performance.
We saw the same pattern: some services requested far more resources than they actually needed, others had overly permissive limits, and a few consumed CPU inefficiently without anyone noticing until costs and latency started creeping up.
We applied three simple resource tuning changes across our JVM workloads. No application rewrites. No infrastructure overhaul. Just data-driven resource tuning that significantly improved cluster efficiency.
Here’s exactly what we changed.
1. The First Fix: Right-Sizing Requests (Stop Over-Reserving CPU & Memory)
The Misuse Pattern
Most teams set this:
resources:
requests:
cpu: "1"
memory: "2Gi"
Not because the service needs it — but because nobody wants the pod to starve.
The problem: Kubernetes schedules based on requests, not limits. If your pod requests too much, the node becomes “full” on paper, even if physical CPU is mostly idle.
This led to:
- wasted node capacity
- unnecessary autoscaling
- cold starts during traffic spikes
- inflated infrastructure costs
What We Did
We analyzed sustained production usage over several weeks rather than relying on theoretical estimates or short-lived spikes.
CPU
Average: 250m
95th percentile: 350m
Request: 400m
Memory
Average: 720Mi
95th percentile: 900Mi
Request: 1Gi
We set requests slightly above the 95th percentile to provide headroom for normal traffic fluctuations while avoiding unnecessary resource reservation.
Result
- 20–25% reduction in cluster nodes
- Improved pod scheduling and autoscaling efficiency
- Eliminated unnecessary resource reservations “just in case”
CPU Request Waste
Before: [ Requested 1.0 CPU ] [ Actual Use: 0.35 ]
After: [ Requested 0.4 CPU ] [ Actual Use: 0.35 ]
2. The Second Fix: Apply CPU Limits (Prevent Runaway Compute Spikes)
The Misuse Pattern : We used to set
limits: {}
Assuming “no limits” = “better performance”.
Reality:
Without CPU limits, a JVM can consume additional CPU during traffic spikes if spare capacity is available on the node. In shared clusters, this may impact neighboring workloads.
On the other hand, overly restrictive CPU limits can introduce CFS throttling, increasing request latency.
The goal is to choose limits that provide sufficient headroom while preventing uncontrolled CPU consumption.
What We Did
We added modest, predictable limits:
limits:
cpu: "700m"
This gave:
- enough headroom
- minimal CPU throttling under normal production load
- controlled GC pacing
- consistent p95/p99 latency
Result
Across our workloads, average CPU consumption dropped by approximately 15% after tuning requests and limits. Actual savings will vary depending on workload characteristics and traffic patterns.
Before vs After
Before:
40%
90%
180% burst
After:
40%
65%
70%
3. The Third Fix: Cap Container Memory (Stop Unbounded Native + Cache Growth)
The Misuse Pattern
Without explicit container memory limits and proper JVM tuning, Java applications can gradually increase memory usage through expanding caches, thread stacks, direct buffers, or even memory leaks. Over time, this can create node-level memory pressure, increasing the likelihood of the Linux OOM Killer terminating the container.
This creates a classic pattern:
“Memory is fine… until suddenly it’s not.”
Then the node kills the pod with OOMKilled.
What We Did
We set:
limits:
memory: "1.5Gi"
And tuned:
-Xmx1024m # keep heap predictable
This gave:
- well-bounded JVM
- predictable GC
- no cache runaway
- no node-level OOM kills
JVM-Specific Tip
Modern JVMs (Java 10+) are container-aware and support options such as:
-XX:MaxRAMPercentage=70
-XX:InitialRAMPercentage=50
instead of relying solely on fixed -Xmx values, especially when deploying across environments with different memory limits.
Real Incident
We once had a service that used ~800Mi on average. No memory limit set.
During a spike, cache grew to 2.1Gi, the JVM itself remained healthy, but the container eventually exceeded the memory available on the node during peak load. The Linux OOM Killer terminated the container, causing multiple pods to restart simultaneously and triggering a chain reaction of rescheduling and traffic redistribution.
Common Mistakes We Saw Everywhere
- Setting CPU requests equal to limits
- Giving every service “1 CPU, 2Gi memory”
- Setting no limits because “limits cause throttling
- Allowing caches to grow unbounded
- Assuming the JVM automatically chooses the right heap size
- Ignoring container-aware JVM settings such as MaxRAMPercentage
- Believing that more memory = more stability
Recommended Baseline Config
The following values are only an example starting point for a typical Spring Boot REST service. Every application has different CPU, memory and latency characteristics, so profile your workloads before adopting similar settings.
resources:
requests:
cpu: "300m"
memory: "512Mi"
limits:
cpu: "600m"
memory: "1Gi"
Micro-Checklist
- Measure actual CPU and memory usage over representative traffic.
- Set requests based on sustained usage with reasonable headroom.
- Configure CPU limits only after evaluating workload latency and throttling metrics.
- Align JVM heap settings (-Xmx or MaxRAMPercentage) with container memory limits.
- Validate changes through load testing.
- Monitor CPU throttling, OOM kills, restart counts and eviction events.
- Review resource settings periodically as workloads evolve.
TL;DR
By right-sizing requests and tuning CPU and memory limits, we significantly reduced Kubernetes resource usage while maintaining stable performance.
- Right-sized requests → improved node utilization and better pod scheduling.
- Modest CPU limits → stable performance, fewer throttles
- Memory caps + JVM heap tuning → no unbounded growth
Small constraints create big efficiency.
What to Do Next
Start with one service.
Measure. Re-size. Then scale the pattern across the cluster.
Resource requests and limits aren’t about restricting applications — they’re about giving Kubernetes and the JVM enough information to make predictable scheduling, scaling, and memory decisions. When tuned using real production metrics, they improve reliability while reducing infrastructure cost.
Thank you for reading. Keep learning, coding and reflecting.
If you liked this breakdown, adding a few claps 👏 can help it reach readers who might find it useful.
메타데이터
- post_id
- 55eef163d738
- slug
- how-we-reduced-kubernetes-resource-usage-through-smarter-resource-tuning-55eef163d738
- url
- https://medium.com/javarevisited/how-we-reduced-kubernetes-resource-usage-through-smarter-resource-tuning-55eef163d738
- canonical_url
- https://medium.com/javarevisited/how-we-reduced-kubernetes-resource-usage-through-smarter-resource-tuning-55eef163d738
- author_url
- https://medium.com/@lu6445899
- status
- ok
- fetched_at
- 2026-07-18 08:45:11