What actually happens when VPA and LimitRange conflict
I've attached VPA to a production namespace and a few days later suddenly got flooded with OOMKilled alerts — I've been there. At first I suspected a memory leak, but the culprit was resource clipping quietly happening between VPA and LimitRange. I spent a long time figuring out why Pods were dying without touching any application code.
VPA automatically adjusts requests/limits based on metrics, and LimitRange enforces resource policies at the namespace level. The moment these two objects coexist in the same namespace, they each try to enforce their own rules without being aware of each other. The problem is that this conflict doesn't surface as a clear error — it appears as silent clipping. It's easy to miss unless you read the events in kubectl describe pod output carefully.
In this post, I'll walk through the exact mechanism behind the conflict, the causes and solutions for each scenario you encounter in real production, and the safe order in which to calculate minAllowed, maxAllowed, and LimitRange max — with concrete numeric examples.
What Happens Internally When VPA Meets LimitRange
VPA's Three Components and Their Roles
VPA is not a single controller — it operates through the cooperation of three components.
The Recommender collects CPU/memory usage from the Kubernetes Metrics API (metrics-server) by default. To use Prometheus directly as the source, you need a separate adapter such as Prometheus Adapter. After analyzing histograms and recording recommendations in the VPA object's status, the Updater determines whether the current Pod's resource settings differ enough from the recommendation to evict it. When a new Pod starts, the Admission Controller (webhook) injects the recommended values into the resources field.
LimitRange kicks in at exactly this final stage. After the Admission Controller injects the recommended values, the API Server's LimitRange admission plugin validates those values and clips them if necessary.
The Three Constraints LimitRange Enforces
apiVersion: v1
kind: LimitRange
metadata:
name: prod-limits
namespace: production
spec:
limits:
- type: Container
min:
cpu: "100m"
memory: "128Mi"
max:
cpu: "4"
memory: "4Gi"
maxLimitRequestRatio:
cpu: "4"
memory: "2"min/max: The absolute range of requests and limits a container can havemaxLimitRequestRatio: The upper bound on thelimits / requestsratio. In the example above,maxLimitRequestRatio: 2for memory means limits cannot exceed twice the requests.
Three Paths to Conflict
At first I thought simply: if VPA's recommendation exceeds the LimitRange max, it just gets clipped to max — but when maxLimitRequestRatio is involved, cascading clipping occurs. When requests go up, limits must go up proportionally, but when limits are capped by max, requests get pushed back down in return.
Causes and Solutions by Real-World Scenario
Scenario 1: OOMKilled Repeating Due to LimitRange max Exceeded
Situation: VPA recommends 6Gi with a LimitRange of memory max 4Gi and maxLimitRequestRatio: 2.
As a result, requests get pushed down to 2Gi, and since the actual application uses 3–4Gi, OOMKilled repeats.
Solution: You must back-calculate LimitRange max taking both VPA maxAllowed and maxLimitRequestRatio into account.
LimitRange max(memory) ≥ VPA maxAllowed(memory) × maxLimitRequestRatio(memory)More practically, it is safer to set VPA maxAllowed first and then set LimitRange max large enough to satisfy the formula above. Explicitly capping VPA maxAllowed to be at or below LimitRange max / ratio, as shown below, prevents clipping from occurring at all.
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: my-app-vpa
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
updatePolicy:
updateMode: "InPlaceOrRecreate"
resourcePolicy:
containerPolicies:
- containerName: app
minAllowed:
cpu: "100m"
memory: "256Mi"
maxAllowed:
cpu: "2"
memory: "3Gi"
controlledValues: RequestsAndLimitsScenario 2: HPA and VPA CPU Conflict
Using CPU-based HPA together with controlledValues: RequestsAndLimits VPA creates a troublesome situation. When VPA raises CPU requests, limits also rise, changing the utilization denominator (total requests) that HPA sees, causing unexpected scale-downs.
spec:
resourcePolicy:
containerPolicies:
- containerName: app
controlledResources: ["memory"]
controlledValues: RequestsAndLimits
minAllowed:
memory: "256Mi"
maxAllowed:
memory: "3Gi"The pattern of letting HPA handle CPU while VPA only adjusts memory is also recommended in the official Kubernetes documentation. Even if you use controlledValues: RequestsOnly for CPU, when used alongside HPA it is safer to remove CPU from controlledResources entirely.
Scenario 3: Pod Creation Failure Due to ResourceQuota Exceeded
The VPA Admission Controller does not check ResourceQuota. This is a structural limitation (GitHub Issue #8401). When VPA raises limits and the namespace-wide limits.memory quota is exceeded, Pod creation fails.
Defensive configuration approach (conceptual example):
VPA maxAllowed ≤ (ResourceQuota limits.memory) / expected max Pod count × safety marginFor example, if the quota is 20Gi, max Pods is 8, and you apply a safety margin of 0.8, then 20Gi / 8 × 0.8 = 2Gi becomes the upper bound for maxAllowed. The safety margin value itself can be adjusted to account for workload variability and temporary overage during rollouts (surge).
Scenario 4: Enabling Recreate Immediately Without Dry-Run → Mass Eviction During Business Hours
There have been cases where a team turned on Recreate mode directly without a dry-run and Pods were evicted one after another during the day.
The safe order is: observe the recommendation patterns in Off mode for at least several days to a week, adjust minAllowed and maxAllowed, and only then activate the update mode.
How to Calculate Recommended Configuration Values
Step-by-Step Calculation Procedure
Step 1 — Collect Baseline (Prometheus Query Examples)
container_memory_working_set_bytes is a gauge metric, so combining rate() + histogram_quantile() is inappropriate. To extract percentiles from a gauge, use quantile_over_time.
# Memory P50 over the last 7 days
quantile_over_time(0.50,
container_memory_working_set_bytes{namespace="production", container="app"}[7d]
)
# Memory P99 over the last 7 days
quantile_over_time(0.99,
container_memory_working_set_bytes{namespace="production", container="app"}[7d]
)
# rate is appropriate for CPU (counter-based)
quantile_over_time(0.99,
rate(container_cpu_usage_seconds_total{namespace="production", container="app"}[5m])[7d:5m]
)Step 2 — Calculate minAllowed / maxAllowed (Conceptual Example)
The multipliers below are not values specified in official documentation — they are starting-point examples for observation in Off mode and subsequent adjustment. Since spike magnitude varies by workload, actual production values must be tuned after observation.
| Parameter | Formula (example) | Notes |
|---|---|---|
Memory minAllowed |
7-day P50 × 1.2 | Buffer for low-traffic periods |
Memory maxAllowed |
7-day P99 × 1.5 | Must be ≤ LimitRange max / ratio |
CPU minAllowed |
7-day P50 × 0.8 | Risk of throttling if set too low |
CPU maxAllowed |
7-day P99 × 2.0 | Avoid overlapping HPA trigger threshold |
If CPU minAllowed is set too low (e.g., below P10), throttling can occur even during low-traffic periods, increasing response latency. It is safer to start near P50 and adjust downward while monitoring the throttle metric (container_cpu_cfs_throttled_periods_total).
Step 3 — Back-Calculate LimitRange max
LimitRange max(memory) ≥ VPA maxAllowed(memory) × maxLimitRequestRatio(memory)Example: with maxAllowed 3Gi and maxLimitRequestRatio 2, LimitRange max must be at least 6Gi or more. Missing this calculation causes the cascading clipping described earlier.
Completed Configuration Example
Since HPA handles CPU, CPU requests/limits are explicitly fixed in the Deployment manifest, and VPA is configured to adjust only memory. The CPU max in LimitRange should account for the maximum replica scenario with HPA scaling and node specs.
apiVersion: v1
kind: LimitRange
metadata:
name: production-limits
namespace: production
spec:
limits:
- type: Container
min:
cpu: "50m"
memory: "128Mi"
max:
cpu: "8"
memory: "8Gi" # VPA maxAllowed 3Gi × ratio 2 = must be 6Gi or more
maxLimitRequestRatio:
cpu: "4"
memory: "2"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
spec:
template:
spec:
containers:
- name: app
resources:
requests:
cpu: "500m" # HPA calculates utilization based on this value
memory: "512Mi" # Adjusted by VPA
limits:
cpu: "1" # Fixed explicitly alongside requests when using HPA
memory: "1Gi" # Adjusted by VPA maintaining ratio
---
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: my-app-vpa
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
updatePolicy:
updateMode: "InPlaceOrRecreate"
resourcePolicy:
containerPolicies:
- containerName: app
controlledResources: ["memory"] # CPU handled by HPA
controlledValues: RequestsAndLimits
minAllowed:
memory: "512Mi"
maxAllowed:
memory: "3Gi"Tradeoffs and Common Mistakes
Tradeoffs in Choosing controlledValues
| Mode | Behavior | Suitable Situation | Caution |
|---|---|---|---|
RequestsOnly |
Adjusts requests only, keeps original limits | CPU + HPA combined use, when you want to fix limits explicitly | Wastes node resources if limits are over-provisioned |
RequestsAndLimits |
Adjusts requests and limits simultaneously at the original ratio | Memory-only VPA, when you want to maintain the limits ratio | Risk of conflict when used with HPA CPU |
Choosing Update Mode
| Mode | Restart | Suitable Workload | Notes |
|---|---|---|---|
Off |
None | Initial observation phase | Generates recommendations only |
Initial |
On first creation only | Workloads where restart is costly | Existing Pods are not changed |
Recreate |
Yes (eviction) | Stateless workloads | Previous default |
InPlaceOrRecreate |
Conditionally none* | Workloads including stateful | Kubernetes 1.35+ (reference) |
*InPlaceOrRecreate adjusts resources without a restart when kubelet can accommodate an in-place resize, and falls back to Recreate (eviction then recreation) when the change exceeds that range or in-place fails.
Auto mode, as of September 2026, is described in the official Kubernetes documentation as "currently behaves the same as Recreate and may switch to in-place updates in the future." To make intent explicit, specifying the actual behavior directly — such as Recreate or InPlaceOrRecreate — leaves less room for misunderstanding.
The Pitfall of minAllowed Configuration
Setting it too high wastes resources during low-traffic periods; setting it too low causes performance degradation during normal operation. Because VPA needs at least several days to over a week of metric history before its recommendations become reliable, the practical approach is not to try to find the perfect value from the start — instead, collect data in Off mode and then converge iteratively.
Attaching Goldilocks (Fairwinds) to a namespace visualizes VPA Off mode recommendations in a web UI, making it easy to extract initial minAllowed/maxAllowed values.
Wrap-Up: Operational Diagnostic Commands
Ultimately, this topic comes down to being able to visually confirm "how the applied values are actually being clipped." The commands below are the best first places to check when a problem occurs.
# What recommendations VPA is actually generating
kubectl describe vpa my-app-vpa -n production
# Final requests/limits injected by the Admission Controller
kubectl get pod <pod-name> -n production -o jsonpath='{.spec.containers[*].resources}'
# Events after LimitRange is applied (traces of clipping)
kubectl describe pod <pod-name> -n production | grep -A5 Events
# Compare namespace quota against actual usage
kubectl describe resourcequota -n production
# Check whether CPU throttling is occurring (validating minAllowed)
kubectl exec <pod-name> -n production -- cat /sys/fs/cgroup/cpu.statVPA is powerful, but when combined with LimitRange, ResourceQuota, and HPA, you need to understand how each layer constrains the others in order for it to behave as intended. The safest approach is: accumulate data in Off mode, derive boundary values using the back-calculation formulas above, verify the actual injected values with diagnostic commands, and then activate the update mode incrementally.
References
- Vertical Pod Autoscaling | Kubernetes Official Documentation
- Limit Ranges | Kubernetes Official Documentation
- Resource Quotas | Kubernetes Official Documentation
- Kubernetes VPA: Architecture, Limitations, and Production Best Practices – ScaleOps
- K8s VPA: Limitations, Best Practices, and the Future of Pod Rightsizing – CloudPilot AI
- Vertical Pod Autoscaling: The Definitive Guide – Povilas Versockas
- Why Kubernetes 1.35 is a game-changer for stateful workload scaling – The New Stack
- VPA Doesn't respect the minAllowed and maxAllowed · Issue #4763 – GitHub
- A new parameter for the management of the limits that the VPA assigns to the new Pod · Issue #7790 – GitHub
- VPA: Pod memory limit exceeds recommendation and namespace quotas · Issue #8401 – GitHub
- Vertical Pod Autoscaling in Azure Kubernetes Service (AKS) – Microsoft Learn
- Goldilocks – FairwindsOps GitHub