Use the Vertical Pod Autoscaler (VPA) to right-size pod CPU/memory requests based on real usage. This is the FinOps answer to over-provisioning: you stop paying for resources you never use, while avoiding throttling and OOMKills.
Prerequisites
Understanding VPA
Unlike the HPA (which changes the number of pods), the VPA changes the size of each pod by adjusting its resource requests. It has three components:
Recommender: observes historical usage and computes target requests.
Updater: evicts pods whose requests are too far from the recommendation.
Admission Controller: rewrites the requests when the pod is (re)created.
Q1: What are the VPA update modes?
Answer
Off: only produce recommendations (no pod is disrupted) — safest, great for analysis.
Initial: apply recommendations only at pod creation.
Recreate: evict running pods and recreate them with the recommended requests.
InPlaceOrRecreate: resize in place when possible (k8s 1.33+), otherwise recreate.
⚠️ Note: the legacy Auto mode is deprecated since VPA 1.7.0 — use the explicit modes above (Auto currently behaves like Recreate).
For production, many teams keep VPA in Off mode and feed recommendations into their sizing/GitOps process.
Q2: Why not combine VPA and HPA on CPU?
Answer
Both would react to the same CPU signal and fight each other: the HPA adds replicas while the VPA changes requests, leading to instability. Safe combinations:
VPA on memory, HPA on CPU.
VPA in Off mode (recommendations only) alongside an HPA.
HPA on custom/external metrics, VPA on CPU/memory.
Setup
Install the lab environment (kind cluster + metrics-server + the official VPA operator) with the dedicated script:
./install.sh
The script installs VPA using the official method documented in the autoscaler repo:
git clone https://github.com/kubernetes/autoscaler.git then ./hack/vpa-up.sh.
The recommender needs a few minutes of metrics before it emits advice.
# Watch until status.recommendation appearskubectl get vpa hamster-vpa -o yaml | grep -A20 "recommendation:"# Or, human readable:kubectl describe vpa hamster-vpa
You should see something like:
Container Recommendations:
Container Name: hamster
Lower Bound:
Cpu: <…>
Memory: <…>
Target:
Cpu: <≈ 500m, much higher than the 50m we requested>
Memory: <…>
Upper Bound:
Cpu: <…>
Target: the requests the VPA recommends (use these for right-sizing).
Lower/Upper bound: the confidence interval.
FinOps takeaway: the gap between the 50m we requested and the Target is exactly the kind of mis-sizing that wastes money (too low → throttling) or money on idle nodes (too high). The VPA quantifies the right value from real data.
Apply the recommendation automatically
Switch the VPA to Recreate mode and watch pods get recreated with the new requests:
The old Auto mode is deprecated since VPA 1.7.0; Recreate is its explicit equivalent.
Watch the requests change as the updater evicts and recreates pods:
kubectl get pods -o custom-columns=\
'NAME:.metadata.name,CPU_REQ:.spec.containers[0].resources.requests.cpu,MEM_REQ:.spec.containers[0].resources.requests.memory' -w
Expected: new pods appear with CPU_REQ close to the VPA Target instead of 50m.
⚠️ Disruption warning:Recreate mode evicts pods. Use a PodDisruptionBudget and never apply it to single-replica critical workloads.
(Bonus) Resize in place, without recreating pods
Since Kubernetes 1.33+, the VPA can resize a running pod without recreating it, using the In-Place Pod Resize feature. The mode is InPlaceOrRecreate: it tries an in-place resize and falls back to Recreate only when needed.
Q4: How is InPlaceOrRecreate different from the HPA?
Answer
Both avoid disrupting traffic, but they act on different axes:
The HPA changes the number of pods (horizontal) and reacts in seconds.
The VPA in-place changes the size (requests) of each existing pod (vertical), based on usage history.
They are complementary, not interchangeable — and must not drive the same CPU/memory metric.
⚠️ Version check: this step requires the API server at v1.33 or newer. On older clusters it silently falls back to Recreate.
kubectl get --raw /version | grep '"minor"'
The previous Recreate step already right-sized the pods, so there is nothing
left to resize. To actually observe an in-place resize, we first reset the pods
back to their 50m baseline:
set the VPA to Off so its admission webhook stops injecting the recommendation;
delete the pods so the Deployment recreates them at the manifest’s 50m.
# 1. Stop the VPA from mutating new pods (admission webhook stays passive)kubectl patch vpa hamster-vpa --type merge \
-p '{"spec":{"updatePolicy":{"updateMode":"Off"}}}'# 2. Recreate the pods at their baseline requestkubectl delete pod -l app=hamster
kubectl rollout status deployment/hamster
# 3. Confirm the requests have reverted to 50mkubectl get pods -l app=hamster \
-o custom-columns='NAME:.metadata.name,START:.status.startTime,CPU_REQ:.spec.containers[0].resources.requests.cpu'# 4. Now switch to in-place mode and watch the live resizekubectl patch vpa hamster-vpa --type merge \
-p '{"spec":{"updatePolicy":{"updateMode":"InPlaceOrRecreate"}}}'
Watch the requests change while the pod keeps the same NAME and START time:
kubectl get pods -l app=hamster -w \
-o custom-columns='NAME:.metadata.name,START:.status.startTime,CPU_REQ:.spec.containers[0].resources.requests.cpu,MEM_REQ:.spec.containers[0].resources.requests.memory'
Expected: unlike Recreate, the pod is not replaced — only its requests are updated live (CPU resizes without restart; a memory change may restart the container depending on its resizePolicy).
# No recommendation after several minutes?kubectl logs -n kube-system deployment/vpa-recommender
# metrics-server must be serving usage data (VPA prerequisite)kubectl top pods -n "$NS"# Remove the VPA operator entirely (official uninstall)# cd /tmp/autoscaler/vertical-pod-autoscaler && ./hack/vpa-down.sh