Kubernetes

CPU/memory requests-limits tuning for JVM services

6 min read Updated Mar 27, 2026

Kubernetes Engineering for Backend Platforms

Kubernetes resources for JVM services look deceptively simple: pick requests, maybe add limits, and move on.

In practice, these settings define three different realities at once:

  • what the scheduler believes the pod needs
  • what the Linux kernel enforces under pressure
  • what the JVM thinks it can safely allocate and keep alive

When those realities disagree, you get the failures teams keep mislabeling as “random pod instability.”

Quick Summary

Decision Good default Main risk if wrong
memory request close to realistic steady-state usage plus headroom node packing becomes fiction
memory limit explicit and intentional OOMKill or hidden overcommit
CPU request enough to protect baseline throughput chronic under-scheduling
CPU limit often avoid or set carefully for latency-sensitive JVMs throttling causes p99 spikes and GC pain
heap sizing tied to container memory, not host assumptions heap competes with native memory and dies late

Part 1 is about the baseline model: how do we pick requests and limits that survive rollout, GC behavior, and saturation?

Start With the Three Memory Buckets

Teams often size a JVM pod as if memory equals heap. It does not.

A Java service in Kubernetes usually needs room for:

  • Java heap
  • native memory, metaspace, thread stacks, direct buffers
  • temporary spikes during startup, JIT warmup, or burst traffic

If you size the memory limit around -Xmx alone, the pod may look fine in quiet tests and then get OOMKilled in production because non-heap memory still counts.

That is why the first sizing conversation should be: “How much total container memory does this service need under realistic concurrency?”

Requests Are Scheduling Truth, Not Documentation

The scheduler places pods based on requests, not observed runtime usage.

If requests are far below reality:

  • the cluster overpacks nodes
  • memory pressure rises
  • eviction risk climbs
  • noisy-neighbor effects become normal

If requests are far above reality:

  • autoscaling signals get distorted
  • cluster utilization looks worse than it is
  • cost rises because spare capacity is locked away

Requests should represent believable steady-state demand with headroom, not optimistic hope.

CPU Limits Are the Most Common Hidden Latency Bug

For many JVM services, especially request-response APIs, memory limits are often necessary but CPU limits deserve caution.

Why:

  • CFS throttling can hit during bursts
  • GC threads get throttled too
  • application threads and GC compete in the same limited CPU budget
  • p99 latency degrades before average CPU dashboards look scary

This is why many teams use:

  • explicit CPU request
  • no CPU limit, or a limit set only after careful measurement

That is not universal advice. It depends on multi-tenant fairness, cluster policy, and workload type. But if a low CPU limit causes “mysterious” latency spikes under burst traffic, throttling should be near the top of the suspect list.

A Better Baseline for JVM Pods

For a service with stable traffic and moderate latency sensitivity, the baseline often looks like:

  1. choose realistic heap size
  2. add explicit non-heap headroom
  3. set memory request near steady-state total usage
  4. set memory limit high enough to absorb burst behavior safely
  5. set CPU request to protect normal operation
  6. add a CPU limit only if policy or multi-tenant control requires it

Example:

resources:
  requests:
    cpu: "500m"
    memory: "1500Mi"
  limits:
    memory: "2Gi"
env:
  - name: JAVA_TOOL_OPTIONS
    value: "-XX:MaxRAMPercentage=65 -XX:InitialRAMPercentage=65"

This is only a starting point. The important part is that heap sizing and container sizing agree with each other.

Tie Heap Policy to Container Reality

Modern JVMs are container-aware, but teams still create trouble by mixing:

  • fixed -Xmx
  • aggressive framework defaults
  • direct buffer usage
  • high thread counts

Good practice is to make one memory strategy obvious:

  • fixed heap with explicit headroom
  • or percentage-based heap sizing tied to container memory

What you want to avoid is accidental drift where the app assumes it owns memory the pod limit does not actually allow.

Rollouts Expose Bad Resource Assumptions Fast

A pod can pass basic load tests and still fail during rollout because rollout changes the shape of pressure:

  • cold JVMs need startup CPU
  • caches are empty
  • JIT warming is incomplete
  • old and new replicas overlap temporarily
  • readiness may turn green before steady latency is real

That means resource tuning must be judged during:

  • scale-out
  • rolling updates
  • failover
  • burst recovery

not just during steady-state one-pod benchmarks.

Signals to Watch Before Changing YAML Again

Measure the platform and JVM together:

  • container memory working set
  • RSS versus heap usage
  • OOMKill count
  • CPU throttling rate
  • GC pause time and allocation rate
  • startup time and readiness delay
  • p95 and p99 latency during rollout

The most useful question is: “Did the pod fail because the application is inefficient, or because the resource envelope is lying?”

Without both JVM and container signals, teams often tune the wrong layer.

Failure Modes That Keep Repeating

Too-small memory limit

The service works until traffic, caches, or direct buffers rise, then dies with OOMKill while heap graphs look deceptively fine.

Too-small request

The cluster packs too many replicas onto a node, and the service becomes unstable only when the node is busy.

Low CPU limit on latency-sensitive service

Median latency stays acceptable while tail latency and GC behavior degrade badly.

Startup profile ignored

Pods are sized for steady state, not warmup. Deployments become the most dangerous time in the lifecycle.

A Practical Tuning Workflow

Use this order:

  1. capture steady-state and rollout metrics
  2. estimate total memory, not heap only
  3. set realistic requests
  4. set memory limits explicitly
  5. question whether CPU limits are helping or hurting
  6. re-run rollout and burst tests

If you skip the rollout and burst step, you are validating only the calmest phase of pod life.

Part 1 Checklist

  • heap sizing and container memory policy agree
  • non-heap headroom is intentional, not accidental
  • requests reflect believable steady-state usage
  • CPU throttling is measured before setting aggressive limits
  • rollout metrics are part of resource tuning, not a separate concern
  • operators can explain whether failure came from JVM behavior or resource envelope mismatch

Key Takeaways

  • Requests and limits are not just YAML. They define how the scheduler, kernel, and JVM interact under pressure.
  • Memory tuning for JVM pods must include non-heap memory, not only -Xmx.
  • CPU limits can be actively harmful for latency-sensitive JVM services if throttling is ignored.
  • A resource policy is only credible after it survives rollout and burst behavior, not just quiet steady state.

Categories

Tags

Continue reading

Previous HPA/VPA interactions and autoscaling anti-patterns