Hepapi Blog - hepapi.com

Stop Guessing Pod Sizes: How In-Place Resizing Made VPA Production Ready

Written by Dilruba Öner | Aug 26, 2026, 7:06:55 AM

Two months ago, you doubled the memory limit of a pod "just to be safe." This morning, you notice on the dashboard that the same service has restarted again. You run kubectl get pod, and the number in the RESTARTS column keeps growing. You run kubectl describe, and there it is at the bottom, that familiar line: OOMKilled. Even double the memory wasn't enough. The same loop, but with a bigger bill.

The real problem isn't the limit; it's the guessing. Setting resource requests by hand feels like a game you're bound to lose: either you carry a huge suitcase that's half empty from place to place, which means waste, and this is exactly where a bloated cloud bill piles up; or you cram everything into a single suitcase that won't close, and you get an OOMKill while forcing the lid shut. Sadly, the middle ground can't be found by hand.

In this article, I'll walk you through VPA (Vertical Pod Autoscaler) from start to finish, and the feature that finally makes it production-ready: in-place resizing.

 

What VPA Is (and What It Isn't)

VPA is a Kubernetes component that monitors the actual CPU/memory usage of your pods and adjusts their request values accordingly. In the suitcase analogy: it measures what you actually put inside and resizes the suitcase to exactly that volume, no more, no less.

The point people get confused about most here is how it differs from HPA: HPA asks, "How many pods?"; VPA asks, "How powerful should each pod be?" For traffic that spikes up and down suddenly, horizontal scaling (HPA) makes sense; for steady, long-running workloads, VPA shines. 💫

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: web-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  updatePolicy:
    updateMode: "Off"
  resourcePolicy:
    containerPolicies:
      - containerName: "*"
        minAllowed:
          cpu: 50m
          memory: 128Mi
        maxAllowed:
          cpu: "1"
          memory: 1Gi

 

How VPA Works: The Three Components

Recommender: It analyzes current and past usage and OOM events; for each container, it produces a target, a lower bound, and an upper bound recommendation. This is the brain.

Updater: It compares running pods against the recommendations and applies the necessary changes. Historically, the only way to do this was to kill the pod (evict it).

Admission Controller: This is a mutating webhook; it injects the recommended values into newly created pods, so a pod starts at the right size from the moment it's born.

The Real Point: Why Wasn't VPA Trusted in Production?

Until recently, applying a recommendation meant killing the pod, because Kubernetes couldn't change the request values of a running pod. For many workloads, this was unacceptable: stateful services, message brokers, JVM-based applications that take a long time to start. While VPA tried to do the "right" thing, it was disrupting production.

The sneakiest example is the vicious circle in cache-heavy workloads. A cache is a piece of memory that takes time to fill. When VPA recreates a pod to apply a recommendation, the new pod starts with an empty cache and uses little memory. VPA mistakes this temporary low number for the "real need" and shrinks the pod even further; then it recreates the pod again, the cache is empty again, and the measurement is misleading again. The loop never closes. So eviction wasn't only disrupting the workload; it was also poisoning VPA's own measurement signal.

The Evolution: In-Place Pod Resize 

And now we've reached the heart of the article. Everything I've described so far, the way eviction causes disruption and the vicious circle in cache-heavy workloads, actually came down to a single root problem: Kubernetes couldn't touch a running pod. If you wanted to change it, you had to kill it first.

This rule was broken in Kubernetes 1.33 (beta); the feature reached stability in version 1.35 (December 2025). Thanks to in-place pod resizing, the kubelet can now change a running pod's CPU and memory limits without stopping it. This is also the finale of our suitcase analogy: we no longer empty the suitcase and move everything into a new one; instead, without touching what's inside, we grow or shrink it right where it is.

This single change also reshaped how VPA works. The modes that decide "how" a recommendation gets applied were renewed:

The old Auto mode is quietly retiring. It never actually did anything magical; behind the scenes, it always behaved like Recreate, which kills the pod.

InPlaceOrRecreate, on the other hand, does the pragmatic thing: "First try gently, without killing. If you can't, fall back to the old way: kill it and recreate it." There are still cases where growing a pod without killing it isn't possible; for example, if the node has run out of space, if the pod's QoS class would change, or if the container says, "restart me when my size changes." In this mode, when VPA hits that wall, it quietly falls back to the old path.

InPlace is the most cautious one: it never kills. If it can't do it now, it doesn't force it; it waits and tries again when it gets the chance.

So what does all this change in practice? In one sentence: shrinking is no longer scary. In the past, shrinking a pod meant killing it and accepting the disruption; that's why most teams never turned shrinking on, leaving plenty of extra resources around. Now that shrinking is risk-free, VPA can act without hesitation and reclaim unused resources. When unused resources are reclaimed, pods pack more tightly onto nodes (binpacking); the tighter they pack, the fewer nodes you need, and this is exactly where the bill really melts away.

There's one more bonus: workloads that were once set aside with "VPA breaks these, stay away," like stateful services such as databases and caches, are now back on the table. Because what broke them wasn't VPA; it was eviction, and that's no longer required.

Hands-On: Let's Try It Safely

It's time to put theory aside and get our hands dirty. We'll work in a small K3s lab; my goal isn't a full tutorial from start to finish, but to share a quick tour where you can try VPA without fear.

Before we start, there's something you need to know: VPA doesn't come built into Kubernetes. Unlike HPA, it isn't already installed in your cluster; you have to install it yourself, from the outside. Let's begin by cloning the Autoscaler repository and running the setup script:

git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler
./hack/vpa-up.sh

Within a few seconds, all three VPA components should be up. Let's check:


If all three show Running, VPA is up and ready. But instead of jumping straight into automatic mode, we start with the most important habit of working with VPA: observe before you trust.

Think of it this way: to choose the right suitcase size, you first need to see how much you actually carry over a few trips. You watch for several trips, measure how much you really use, and only then decide on the suitcase size. The same applies to VPA, because a freshly installed VPA doesn't know the workload yet, and its first recommendations may be off. That's why we first give it only the role of an "observer that takes measurements," without touching anything.

The mode that makes this possible is Off. In this mode, VPA regularly produces recommendations for the pods it watches (the Target, Lower Bound, and Upper Bound values we'll see shortly), but it doesn't apply them; it neither kills nor resizes the pods. So it's completely harmless; you can turn it on without fear even in production, because the only thing it can do is jot down a note on the side: "I think this pod needs about this much resource."

VPA doesn't work on its own either; for it to observe something, you have to give it a target, that is, an application. So first, we create an example web deployment for VPA to watch:

# web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:stable
          resources:
            requests:
              cpu: 50m
              memory: 64Mi

A few minutes later, let's see the recommendations:

kubectl describe vpa web-vpa

In my screenshot, the Target shows 50m, while the Uncapped Target shows 25m. The reason is that my VPA object also has a minAllowed limit defined; it pushed VPA's raw 25m recommendation up by saying, "don't go below 50m." If you set up VPA in its plain form without minAllowed, these two values will most likely come out equal, so don't worry; this is completely normal. The two values diverge only when a lower or upper bound is in effect.

Among these three values, the one to keep your eyes on is Target; it's the number where VPA says, "this pod actually needs about this much request." Lower Bound and Upper Bound are the lower and upper limits of the safe range. But don't ever look at the first Target you see and turn automation on. The rule is simple: watch for 1-2 weeks, see how steady the Target stays day by day, and only switch to automatic mode once you're convinced it's stable.

 

Common Pitfalls

These are the most common pitfalls that teams using VPA in production, and the official docs, underline again and again. It's worth taking a look before you experiment.

Binding HPA and VPA to the same metric causes problems. If both are set up to monitor CPU, for example, they clash: one adds replicas, the other scales the pod, and they keep overriding each other's decisions. The common advice is to separate responsibilities: let the replica count be HPA's job and memory be VPA's.

"In-place" doesn't always mean in-place. As we saw earlier, in some cases VPA has to fall back to the old method, recreate. So relaxing completely because "pods don't die anymore" can be misleading; it's considered safer to plan your PDBs and replica counts assuming an eviction can still happen once in a while.

Recommendations that keep jumping are a signal. If the Target value varies widely from day to day, this usually isn't a VPA malfunction but rather reflects the nature of the workload: either the traffic is very sudden and irregular (the kind VPA doesn't handle well), or not enough historical data has built up yet. In this case, it's recommended to wait before enabling automation.

Keep your expectations for fine-tuning low for now. The Recommender today largely operates globally; the ability to fine-tune per workload remains limited. The good news is that this side is improving fast; upstream is discussing improvements around VPA 1.6.

 

Conclusion

Remember the scene from the beginning: the limits you inflated "just to be safe," and two months later, OOMKilled again. Our problem was never really the limit; it was failing to pick the right suitcase. Either we were carrying a huge, half-empty suitcase, or we were cramming everything into one that wouldn't close.

VPA makes this guess for us: it looks at what you actually carry and brings the suitcase to exactly that size. But there was a problem that tied its hands for a long time; to grow the suitcase, you first had to empty it and move everything into a new one, that is, kill the pod. And this is exactly what in-place resizing changed: we can now grow or shrink the suitcase right where it is, without touching what's inside. No killing, no disruption, none of that sneaky vicious circle. So VPA stops being just a "recommendation engine" and turns into a rightsizing layer that runs continuously in the background.

It's not the cure for every workload, of course; sudden traffic is still HPA's job, and the JVM side still needs a bit of attention. But the days of guessing resource sizes by hand are slowly coming to an end. If you want to try it, the safest way to start is with one sentence: set it up in Off mode, watch for a week, then decide.

Thanks for reading.

Further Readings