← Back to list

Kubernetes Resource Requests Are a Lie. How to Fix Them

How to use VPA and Goldilocks to right-size production workloads without breaking SLOs

Tom Jose in kotaicode · 2026-06-22 12:36 · 25 claps · 5.4 min read
#kubernetes #devops #aws #software-engineering #cloud-computing
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud

Kubernetes Resource Requests Are a Lie. How to Fix Them

How to use VPA and Goldilocks to right-size production workloads without breaking SLOs

Open your cluster right now and run this:

kubectl top pods -A | sort -k3 -rn | head -20

Then run this:

kubectl get pods -A -o json | jq -r '.items[] | 
  .metadata.namespace + "/" + .metadata.name + " requests: " + 
  (.spec.containers[0].resources.requests.cpu // "none") + 
  " / " + (.spec.containers[0].resources.requests.memory // "none")'

Compare the two outputs. In most clusters, the gap between what pods request and what they actually use is enormous. A pod requesting 2 CPU cores using 0.08. A pod requesting 4Gi memory sitting at 400Mi. Multiply that across hundreds of pods and you have a cluster that’s simultaneously over-provisioned on paper and perpetually short on schedulable capacity.

Resource requests aren’t a technical problem. They’re a social one. Engineers set them high at deployment time because nobody wants to be the person who caused an OOMKill. They never get revisited because there’s no forcing function. They accumulate until a FinOps audit finds them or the compute bill forces the conversation.

Here’s how to make them accurate — without breaking anything that’s already running.

Why wrong requests cause real problems

Resource requests in Kubernetes do two things: they influence scheduling decisions and they affect quality of service. The scheduler uses requests — not limits — to decide which node has room for a pod. If you request 2 CPU and use 0.08, you’re holding 1.92 CPU hostage from the scheduler. That node shows as full when it has real capacity available.

Over-provisioning requests — nodes fill up on paper before they fill up in reality. Cluster autoscaler adds nodes that aren’t needed. You’re paying for compute headroom that exists only in your YAML.

Under-provisioning requests — the scheduler places too many pods on a node. When real load hits, pods compete for CPU and memory. The ones with low requests get throttled first. SLOs break.

Neither extreme is safe. The only answer is accuracy — and accuracy requires data.

The Vertical Pod Autoscaler — recommendations without enforcement

VPA observes actual resource usage over time and generates recommendations. Optionally it applies those recommendations by restarting pods — but most teams should start with recommendation mode only. You get the data without the risk of VPA restarting production pods at an inconvenient moment.

Install via Helm:

helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm install vpa fairwinds-stable/vpa \
  --namespace vpa \
  --create-namespace \
  --set recommender.enabled=true \
  --set updater.enabled=false \
  --set admissionController.enabled=false

updater.enabled=false and admissionController.enabled=false puts VPA in pure recommendation mode — it watches and suggests, never acts.

Create a VPA object per workload:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-server-vpa
  namespace: production
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  updatePolicy:
    updateMode: "Off"  # recommendations only, no automatic updates
  resourcePolicy:
    containerPolicies:
      - containerName: api-server
        minAllowed:
          cpu: 50m
          memory: 64Mi
        maxAllowed:
          cpu: 2000m
          memory: 2Gi

After 24–48 hours, read the recommendations:

kubectl get vpa api-server-vpa -n production -o yaml

The status.recommendation section is what you're after:

status:
  recommendation:
    containerRecommendations:
    - containerName: api-server
      lowerBound:
        cpu: 80m
        memory: 120Mi
      target:
        cpu: 150m
        memory: 256Mi
      upperBound:
        cpu: 400m
        memory: 512Mi

target is what VPA recommends for normal operation. upperBound is a buffer for traffic spikes. Use target as your request value and upperBound as your limit — not the other way around.

The sharp edge: VPA recommendations are only as good as the observation window. 18 hours of data misses weekend patterns, batch jobs, and month-end load. Give it at least 7 days, ideally 14, before trusting numbers for production workloads.

Goldilocks — recommendations at scale

VPA per-workload works for a handful of deployments. For a cluster with 50 or 200 workloads, Goldilocks creates VPA objects for every Deployment in a namespace and surfaces everything through a single dashboard.

helm install goldilocks fairwinds-stable/goldilocks \
  --namespace goldilocks \
  --create-namespace
kubectl label namespace production goldilocks.fairwinds.com/enabled=true
kubectl label namespace staging goldilocks.fairwinds.com/enabled=true

Access the dashboard:

kubectl port-forward svc/goldilocks-dashboard 8080:80 -n goldilocks

The dashboard shows current requests alongside VPA’s lowerBound, target, and upperBound for every workload — and generates the Helm values diff to apply each recommendation. For a workload running with inflated requests, it looks like this:

# Goldilocks suggested values for api-server
resources:
  requests:
    cpu: 150m      # was 2000m
    memory: 256Mi  # was 2Gi
  limits:
    cpu: 400m      # was 2000m
    memory: 512Mi  # was 2Gi

What to look for in the dashboard:

  • Workloads where current CPU request is 10x the target — biggest savings, lowest risk
  • Workloads where current memory request is below lowerBound — OOMKill risk hiding in plain sight
  • Workloads with wide lowerBound-to-upperBound gaps — spiky traffic, needs careful limits

Applying recommendations without breaking SLOs

The wrong way to apply right-sizing is the common way: copy the VPA target directly into your Deployment manifests, apply everything at once, and wait to see what breaks.

# Wrong — limits equal to requests, applied all at once
resources:
  requests:
    cpu: 150m
    memory: 256Mi
  limits:
    cpu: 150m      # same as request — any spike gets throttled immediately
    memory: 256Mi  # same as request — any spike OOMKills the pod

Setting limits equal to requests is the most common right-sizing mistake. It feels precise. In practice, any traffic spike that pushes usage above the request immediately hits the limit — CPU gets throttled, memory triggers an OOMKill. You’ve traded over-provisioning for brittleness.

# Right — limits above requests, applied in stages
resources:
  requests:
    cpu: 150m      # VPA target
    memory: 256Mi  # VPA target
  limits:
    cpu: 400m      # VPA upperBound — headroom for spikes
    memory: 512Mi  # VPA upperBound — headroom for spikes

The staged rollout pattern:

1. Start with non-critical workloads. Background workers, batch jobs, internal tooling. Apply recommendations directly. Watch for two weeks. Build confidence before touching customer-facing services.

2. Step down in stages for production workloads. Don’t jump from 2000m to 150m in one change. Go to 800m first, observe for a week, then to 400m, then to target. Each step is a smaller blast radius.

3. Watch these for 48 hours after each change:

# OOMKill events
kubectl get events -n production --field-selector reason=OOMKilling
# HPA unexpected scaling — signals resource pressure
kubectl get hpa -n production -w
# CPU throttling visible in metrics-server
kubectl top pods -n production

The VPA and HPA conflict: never enable VPA’s Auto mode on a workload that uses HPA. They fight — VPA resizes pods while HPA tries to scale them horizontally. Keep VPA in Off mode for any HPA-managed workload and apply recommendations manually.

The things that will trip you up

Memory recommendations lag real spikes. VPA’s memory recommender uses a percentile model that dampens short spikes. A monthly batch job that doubles memory usage for two hours may not appear in a 7-day window. Check usage graphs for the full billing cycle before trusting memory numbers.

Namespace ResourceQuotas block VPA updates. If a namespace quota caps memory at 4Gi and VPA tries to schedule a pod with 512Mi — down from 2Gi — the math still works. But if VPA recommends up (catching an under-provisioned workload), the pod can fail to start if the quota is already tight. Audit quotas before enabling Auto mode.

VPA needs metrics-server access. In clusters with restrictive RBAC, recommendations stay empty indefinitely. If nothing appears after 48 hours, check VPA recommender logs before assuming the tool doesn’t work.

Before and after

Metric Before After Total requested CPU (47 deployments) 94 cores 31 cores Node count (m5.4xlarge) 6 4 SLO breaches during transition — 0 Monthly compute cost baseline −33%

The cluster wasn’t under-engineered. The resource requests were over-engineered — set once, never revisited, accumulated across two years of deployments. Fixing them didn’t require more monitoring, more engineering, or more risk tolerance. It required looking at data VPA had already collected and acting on it.

Run kubectl top pods. Deploy VPA in recommendation mode. Open Goldilocks in a week.

The numbers will be uncomfortable. That’s the point.

Tom Jose is a DevOps Engineer at Kotaicode, where the team helps companies get their Kubernetes and AWS infrastructure production-ready.


메타데이터
post_id
898fce342f43
slug
kubernetes-resource-requests-are-a-lie-how-to-fix-them-898fce342f43
url
https://medium.com/kotaicode/kubernetes-resource-requests-are-a-lie-how-to-fix-them-898fce342f43
canonical_url
https://medium.com/kotaicode/kubernetes-resource-requests-are-a-lie-how-to-fix-them-898fce342f43
author_url
https://medium.com/@Tomjosetj31
status
ok
fetched_at
2026-06-23 03:48:11