Kubernetes In-Place Pod Resize: Rightsizing Without Restarts (2026)

Kubernetes In-Place Pod Resize: Rightsizing Without Restarts (2026)

Kubernetes In-Place Pod Resize: Rightsizing Without Restarts (2026)

For a decade, changing a container’s CPU or memory in Kubernetes meant one thing: recreate the Pod. Kubernetes in-place pod resize breaks that rule. You now patch spec.containers[].resources on a running Pod, and the kubelet rewrites the container’s cgroup limits live — no eviction, no new sandbox, no cold start. The feature (KEP-1287) went alpha in v1.27, graduated to beta in v1.33 in May 2025, and reached stable/GA in v1.35 on 19 December 2025. That last milestone matters: GA means the API surface, the /resize subresource, and the status conditions are now a contract you can build automation against.

This is a mechanism-level walkthrough for platform and SRE engineers who already run production clusters and want to know exactly what happens between kubectl patch and a changed memory.max.

What this covers: the resize subresource and resizePolicy; kubelet, CRI, and cgroup v2 actuation; the PodResizePending/PodResizeInProgress conditions; Vertical Pod Autoscaler in-place mode; memory-decrease and OOM risk; QoS and node-pressure interactions; and the FinOps case for rightsizing without disruptive restarts.

Context and Background

The original resource model treated resources.requests and resources.limits as immutable after Pod admission. The scheduler read requests once to place the Pod, the kubelet wrote cgroup limits once at container start, and that was the end of it. To give a container more memory, you edited the Deployment, and the controller rolled Pods — terminating old ones and scheduling new ones somewhere with capacity.

For stateless web services that pattern is merely wasteful. For anything with warm state it is genuinely painful. A JVM that spent ninety seconds building its JIT profile, an in-memory cache that took twenty minutes to fill, a database replica mid-catch-up, a Kafka consumer replaying a partition — every one of these pays a real cost when the Pod restarts to gain a few hundred megabytes. The “resize equals recreate Pod” tax is why so many teams over-provision from day one: it is cheaper to pad every request by 50% than to absorb a restart storm later. That structural over-provisioning is the same waste we quantified in our guide to Kubernetes cost optimization and GPU rightsizing.

The Vertical Pod Autoscaler existed, but it worked by evicting Pods to apply new recommendations. That made VPA a non-starter for exactly the stateful, latency-sensitive workloads that most need rightsizing. The Kubernetes project tracked this gap for years; the official v1.33 beta announcement frames in-place resize as the missing primitive that finally lets vertical scaling happen without disruption. GA in v1.35 turned it from an experiment into a foundation.

It helps to hold the version timeline precisely, because a lot of blog content conflates the milestones. KEP-1287 landed as alpha in v1.27 in 2023 behind the InPlacePodVerticalScaling feature gate, disabled by default. It sat in alpha longer than most features — the memory-decrease and QoS edge cases were genuinely hard — before graduating to beta in v1.33, at which point the gate flipped on by default and the /resize subresource became the supported entry point. Stable arrived in v1.35 in December 2025, more than six years after the idea was first proposed. When you read a tutorial, check which version it targets: the v1.33 beta era deprecated the Pod.Status.Resize field that older examples still reference, and only v1.35 permits memory-limit decreases at all. Treating a v1.33 tutorial as gospel on a v1.35 cluster will mislead you on exactly the two behaviours most likely to bite in production.

How in-place resize works

In-place resize makes spec.containers[].resources mutable for CPU and memory. You submit the change through a dedicated /resize subresource; the kubelet on the Pod’s node accepts the new desired state, allocates against node capacity, calls the container runtime to rewrite the container’s cgroup v2 limits, and records progress in Pod status conditions. Nothing kills the process — the running container keeps its PID, its file descriptors, and its memory.

Sequence diagram of a Kubernetes in-place pod resize from kubectl patch through the API server, kubelet, CRI runtime, and cgroup v2 write

Figure 1: The resize path. Long description: kubectl sends a patch to the /resize subresource on the API server, which validates and admits the change and writes the desired resources into the Pod spec. The kubelet observes the new spec, allocates resources and sets the PodResizeInProgress condition, then issues an UpdateContainerResources call over the CRI to the runtime. The runtime writes cpu.max and memory.max into the container’s cgroup v2, confirms the change, and the kubelet updates status.containerStatuses[].resources to reflect what is now live.

The resize subresource and resizePolicy

The /resize subresource is the correct entry point, and it exists for a reason. Before it, changing resources on a live Pod risked clobbering other spec fields through a naive PUT. The subresource narrows the write to the resource stanza only. From the CLI:

kubectl patch pod resize-demo --subresource resize --type strategic \
  -p '{"spec":{"containers":[{"name":"app","resources":{"requests":{"cpu":"500m","memory":"512Mi"},"limits":{"cpu":"1","memory":"1Gi"}}}}}}'

The --subresource=resize flag requires a kubectl client of at least v1.32. Under the hood this is an ordinary API write against a subresource, so controllers, operators, and client-go all reach it the same way — you are not restricted to kubectl.

Each container can declare a resizePolicy that tells the kubelet, per resource, whether a change can be applied live or demands a restart:

resizePolicy:
  - resourceName: cpu
    restartPolicy: NotRequired
  - resourceName: memory
    restartPolicy: RestartContainer

NotRequired (the default when you omit the policy) means “patch the cgroup on the running process.” RestartContainer means “this container cannot safely absorb a change to this resource while running — restart it in place to apply the new value.” The classic case for RestartContainer on memory is a JVM with a fixed heap sized from -Xmx at launch: handing the cgroup more memory does nothing for a process that already decided its heap ceiling, so you restart to let it re-read the limit. Note the scope — RestartContainer restarts the single container inside the existing Pod; it does not recreate the Pod, reschedule it, or change its IP.

Kubelet, CRI, and cgroup v2 actuation

The actuation depends entirely on the cgroup v2 unified hierarchy, which lets the kubelet rewrite a running process’s resource boundaries without signalling it. When the kubelet accepts a resize, it calls UpdateContainerResources over the Container Runtime Interface to containerd or CRI-O, and the runtime writes the new values into the cgroup: cpu.max for the CPU quota, cpu.weight for the shares that back the request, and memory.max for the memory limit. The process never sees a signal; from inside the container, the ceiling simply moves.

CPU is the well-behaved dimension. Raising or lowering cpu.max takes effect on the next scheduling period, so a CPU resize is close to instantaneous and safely reversible — the kernel just enforces a different quota. There is no state to lose because CPU is a rate, not a reservation.

Memory is asymmetric and this is the single most important mechanic to internalise. Increasing memory.max is trivial and safe. Decreasing it is dangerous, because you cannot shrink a limit below what the process is already using without inviting the OOM killer. A subtlety that changed at GA: memory-limit decreases were disallowed entirely before v1.35, and are now permitted but gated by a best-effort kubelet check. If current usage exceeds the proposed new limit, the kubelet refuses to apply the shrink and parks the resize as pending rather than risk an OOM-kill. It is explicitly best-effort — there is a race where usage can spike right after the check — so a memory decrease is never something to treat as guaranteed.

Also worth noting: the v1.33 update to the CRI contract specified that runtimes must not deliberately restart a container to adjust resources. That closed a loophole where a runtime could technically satisfy UpdateContainerResources by bouncing the container — defeating the entire point.

There is a scheduling wrinkle behind the actuation that trips people up. When the kubelet accepts a resize, it does not consult the scheduler. Placement happened once, at admission; a resize is a node-local operation that the kubelet allocates against the node’s own accounting of allocatable capacity minus what is already committed. This is why an in-place resize can never move a Pod, and why the failure mode for “no room” is a pending condition rather than a reschedule. It also means the scheduler’s view and the kubelet’s view can briefly diverge: the API server records the new requests immediately, but the scheduler’s cache reflects them on its next sync. For a single resize that skew is invisible; for a burst of VPA-driven resizes across many Pods on one node, it is the kind of thing that shows up as transient over-commit in your capacity dashboards. Account for it rather than being surprised by it.

One more mechanic worth internalising: requests and limits actuate through different cgroup knobs and have different semantics. The CPU request backs cpu.weight, which only matters under contention — it decides proportional share when the node is saturated. The CPU limit backs cpu.max, a hard quota enforced every scheduling period regardless of contention. Memory request is purely a scheduling and eviction-ranking input; it is not a cgroup enforcement value at all. Memory limit is memory.max, the hard ceiling the OOM killer watches. When you resize, be clear about which of these four you are actually moving, because raising a request and raising a limit have completely different runtime effects — one changes how the Pod competes and how it is ranked under node pressure, the other changes the hard wall it hits.

Conditions and status you actually watch

GA replaced the old, deprecated Pod.Status.Resize field with two proper conditions, and any automation you build must key off these rather than the removed field. PodResizeInProgress means the kubelet accepted the resize and allocated resources and is applying the change — usually brief, occasionally longer depending on the resource and runtime. PodResizePending means the kubelet cannot grant the request right now, with a reason that is either Infeasible (impossible on this node — for example a CPU request larger than the node’s allocatable) or Deferred (not possible right now but might become feasible later, such as a memory decrease blocked by live usage).

Two status fields tell you desired versus actual. spec.containers[].resources is what you asked for; status.containerStatuses[].resources is what is currently live in the cgroup. The two converge when a resize completes, and watching the gap between them is how you confirm actuation rather than mere acceptance. The conditions also carry observedGeneration, tying each pending or in-progress resize back to the specific metadata.generation of the Pod spec that triggered it — essential when resizes arrive faster than they complete.

The observedGeneration detail deserves emphasis because it is what makes resize automation robust. Each write to the Pod spec bumps metadata.generation. When the kubelet sets PodResizeInProgress, it stamps the condition with the generation of the spec it is actuating; when it sets PodResizePending, it stamps the generation at which allocation was last attempted. If you patch a resize, then patch again before the first completes, you can tell from observedGeneration whether the condition you are reading reflects your latest intent or a stale one. Without it, a controller polling status has no reliable way to distinguish “my new resize is still pending” from “an old resize is pending and my new one has not been observed yet.” Any operator you write around resize should compare the condition’s observedGeneration against the current metadata.generation before deciding a resize has settled.

A practical corollary: do not treat the absence of a resize condition as success on its own. A completed resize clears the in-progress condition, but so does a resize that never started because your patch was a no-op or was rejected at admission. Confirm success by checking that status.containerStatuses[].resources actually equals your desired spec.containers[].resources for the resource you changed. That equality is the ground truth; the conditions are the narration.

VPA + rightsizing walk-through

Manual resize is a fine primitive, but rightsizing at fleet scale needs a controller. That is the Vertical Pod Autoscaler, and the interesting question in 2026 is how VPA uses in-place resize instead of eviction.

Flowchart of the Vertical Pod Autoscaler in-place update path with recommender, updater, in-place attempt, and evict fallback

Figure 2: Per-resource resizePolicy branching. Long description: a resize request for new CPU or memory reaches a decision on the container’s resizePolicy for that resource. A NotRequired policy patches the cgroup live and the container keeps running. A RestartContainer policy restarts the container to apply the new value, and the process restarts inside the same Pod sandbox without rescheduling.

VPA has three components. The recommender watches historical and live usage and computes a target, a lower bound, and an upper bound per container. The updater decides when a running Pod has drifted far enough from its recommendation to act. The admission controller injects recommended resources into new Pods at creation. Historically the updater’s only lever was eviction, which is why VPA and stateful workloads never mixed.

Flowchart of the VPA recommender feeding the updater, which attempts an in-place resize and falls back to evicting the pod when in-place is not feasible

Figure 3: The VPA in-place decision. Long description: the recommender reads metrics and produces a target recommendation, which the updater consumes in InPlaceOrRecreate mode. The updater tests whether an in-place resize is feasible. If yes, it patches the /resize subresource and the Pod is resized without a restart. If no, it evicts the Pod as a fallback, and the workload controller recreates it with the new size.

The new update mode is InPlaceOrRecreate. In this mode the updater tries an in-place resize first, and only if the in-place path cannot complete in time does it fall back to eviction. Be precise about status here, because it is a separate release train from core Kubernetes: InPlaceOrRecreate shipped as an alpha feature gate in the VPA project (kubernetes/autoscaler) starting around VPA 1.4, off by default, and has been progressing toward beta and eventual stable. You opt in by passing --feature-gates=InPlaceOrRecreate=true to the updater and admission controller and setting the VPA object’s updateMode accordingly. Treat any single-source “it is GA now” claim with caution and pin your behaviour to the VPA release you actually deploy.

VPA’s in-place trigger logic is worth knowing so you can predict when it will act. It applies an in-place update when a container’s request is below the recommendation’s lower bound, or above its upper bound, or when the gap between the sum of Pod requests and the sum of recommendation targets exceeds roughly 10% and the Pod has run undisrupted for at least 12 hours. That last clause deliberately avoids thrashing well-behaved Pods over small deltas.

The two bounds do different jobs and it is worth separating them. The lower and upper bounds are safety rails — cross them and VPA acts promptly because the Pod is meaningfully mis-sized in a way that risks either waste or throttling. The 10%-plus-12-hours rule is the steady-state nudge that gently converges a Pod that is only modestly off target, without reacting to every diurnal spike. In practice this means a Pod that gets suddenly starved (request far below lower bound) is corrected fast, while a Pod that is merely 15% oversized drifts down over a day. If you want tighter convergence you can narrow the recommender’s margins, but resist the urge to make VPA aggressive on stateful workloads — every extra resize is another chance to hit the eviction fallback.

Because that fallback is the sharp edge, design for it explicitly. The name InPlaceOrRecreate reads as “in-place, with recreate as a rare backstop,” but on a busy cluster the recreate path fires more often than teams expect: any resize the node cannot satisfy in-place becomes an eviction. For a StatefulSet or a workload with a PodDisruptionBudget, that eviction is not free — it can block, or it can cascade through your replicas one at a time. The safe posture is to assume every VPA-managed Pod might be evicted at any recommendation change, set a PDB that reflects the real availability you need, and give the cluster node headroom so the in-place path is the one that actually runs. If you cannot tolerate eviction at all, do not use InPlaceOrRecreate for that workload — use manual, monitored resizes instead.

A minimal VPA that prefers in-place:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: web-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  updatePolicy:
    updateMode: InPlaceOrRecreate
  resourcePolicy:
    containerPolicies:
      - containerName: app
        controlledResources: ["cpu", "memory"]
        controlledValues: RequestsAndLimits

Whether a given change stays in-place or falls back to a restart is a policy decision, not luck. Use this matrix to reason about it:

Change resizePolicy Typical outcome Why
CPU increase NotRequired In-place, near-instant cpu.max widens on next period
CPU decrease NotRequired In-place Quota narrows, no state at risk
Memory increase NotRequired In-place memory.max raised safely
Memory decrease, usage below new limit NotRequired In-place Best-effort check passes
Memory decrease, usage above new limit NotRequired Deferred, held Kubelet avoids OOM-kill
Memory change for fixed-heap runtime RestartContainer Container restart, same Pod Process must re-read limit
CPU increase, node lacks capacity NotRequired Infeasible, pending No allocatable headroom on node

The pattern that falls out of this table: set NotRequired for CPU almost always, and reserve RestartContainer for memory on runtimes that fix their heap at launch. Pair VPA-driven vertical rightsizing with node-level elasticity so that Infeasible resizes get somewhere to land — this is exactly where a fast node autoscaler earns its keep, as we covered in the Karpenter node autoscaling deep dive.

The FinOps case is the reason any of this matters to a budget, not just an SRE. Cluster spend is driven by requests, not usage — the scheduler bin-packs on requests, and unused requested capacity is capacity you pay for and nobody uses. Before in-place resize, the only safe way to right a mis-sized request was a restart, and because restarts hurt, teams padded requests generously and left them padded. That padding is pure margin the cloud provider keeps. In-place resize removes the restart penalty that made padding rational, so a VPA loop can now walk requests down toward real usage continuously, on live Pods, without anyone flinching. The compounding effect on a large cluster is the interesting part: shaving 20-30% off aggregate requests raises effective bin-packing density, which means fewer nodes, which is a direct line-item saving. And unlike a one-time rightsizing sweep, an in-place VPA loop keeps the savings as workloads evolve rather than letting drift re-inflate them. The catch, and it is a real one, is that aggressive downsizing without headroom converts saved money into Infeasible resizes and throttling — so the cost win is only durable when paired with the node elasticity above.

Trade-offs, Gotchas, and What Goes Wrong

Flowchart showing memory-decrease and CPU-increase resize requests branching into deferred, applied, infeasible, or in-place outcomes

Figure 4: Two failure-prone paths. Long description: a memory-decrease request checks whether current usage is above the new limit; if yes the resize is held as Deferred to avoid an OOM-kill, if no the new memory.max is applied. A CPU-increase request checks whether the node has free capacity; if not the resize is marked Infeasible and left pending, if so it is resized in place.

The memory-decrease OOM risk is the headline gotcha. The kubelet’s guard is best-effort and racy: a check that passes can be invalidated microseconds later by an allocation spike. Never script aggressive downward memory resizes on latency-critical services without headroom; treat a memory shrink as an eventual, not immediate, operation.

QoS class is effectively frozen. A Pod’s QoS class — Guaranteed, Burstable, or BestEffort — is derived at admission and does not change when you resize. You cannot resize a Burstable Pod into Guaranteed by matching its requests and limits on a live Pod, and the kubelet rejects resizes that would imply a class change. Plan the class up front.

Node fit is not guaranteed. In-place resize allocates against the node the Pod already runs on. There is no rescheduling — a Pod cannot resize its way onto a different node. If the node lacks headroom the resize sits Infeasible until capacity appears, which is a real hazard on tightly-packed nodes and interacts badly with the multi-cluster and Cluster API topologies where scheduling assumptions vary per cluster.

Controller races are the operational trap. If a Deployment controller, a GitOps reconciler, and VPA all believe they own resources, they will fight — one writes the recommended value, another reverts to the manifest, and the Pod flaps. Decide on a single owner per field, and if VPA owns resources, keep them out of your Git-managed manifest or scope your reconciler to ignore that path. The silent-fallback behaviour of InPlaceOrRecreate compounds this: a resize you expected to be seamless can quietly become an eviction when the node cannot satisfy it, so alert on eviction events even when you think you are running fully in-place.

Practical Recommendations

In-place resize is production-ready at GA, but adopt it deliberately rather than flipping VPA to InPlaceOrRecreate fleet-wide on day one.

  • Confirm the substrate. Verify nodes run cgroup v2 (the unified hierarchy is mandatory) and that your control plane and nodes are at v1.35+ for GA behaviour, including memory-decrease support.
  • Watch conditions, not the old field. Build dashboards and alerts on PodResizeInProgress and PodResizePending with its reason; the deprecated Pod.Status.Resize is gone.
  • Set resizePolicy explicitly. Default CPU to NotRequired; use RestartContainer for memory only on fixed-heap runtimes. Do not rely on implicit defaults in critical paths.
  • Give resizes somewhere to land. Pair vertical rightsizing with a fast node autoscaler so Infeasible states resolve instead of lingering.
  • Establish single ownership of resources per workload — VPA or your manifest, never both — and alert on unexpected evictions.
  • Roll out in stages. Start VPA in Off to observe recommendations, move to Initial, then graduate steady, well-understood workloads to InPlaceOrRecreate.
  • Treat memory shrink as eventual. Assume the best-effort guard, keep headroom, and never assume a downward memory resize applies instantly.

Checklist before enabling in production: cgroup v2 confirmed; kubectl and cluster at compatible versions; resize conditions monitored; resizePolicy set per container; node autoscaling in place; resource ownership single-writer; eviction alerts wired; staged VPA rollout planned.

Frequently Asked Questions

Is Kubernetes in-place pod resize GA in 2026?

Yes. KEP-1287 graduated to stable/GA in Kubernetes v1.35, released 19 December 2025, after going alpha in v1.27 and beta in v1.33. GA means the /resize subresource, the resizePolicy field, and the PodResizePending/PodResizeInProgress conditions are stable API that you can build automation against without expecting breaking changes.

Which resources can I resize without a restart?

CPU and memory requests and limits. CPU changes apply near-instantly by rewriting cpu.max and cpu.weight on the next scheduling period. Memory increases apply live. Memory decreases apply only when current usage is below the proposed limit; otherwise the kubelet holds the resize as Deferred to avoid an OOM-kill. Other resource types are out of scope for in-place resize.

What is the difference between NotRequired and RestartContainer in resizePolicy?

NotRequired, the default, tells the kubelet to apply the change to the running container by patching its cgroup, with no restart. RestartContainer tells the kubelet the container must restart to pick up the change — used for runtimes that read a resource once at launch, like a JVM with a fixed heap. RestartContainer restarts only that container inside the existing Pod; it never reschedules the Pod.

Does in-place resize change a Pod’s QoS class?

No. The QoS class is fixed at admission and immutable for the Pod’s lifetime. You cannot resize a Burstable Pod into Guaranteed, and the kubelet rejects any resize that would imply a QoS class change. If you need a specific QoS class, set requests and limits to achieve it when the Pod is first created.

How does VPA use in-place resize?

Through the InPlaceOrRecreate update mode. The VPA updater attempts an in-place resize first and only evicts the Pod if in-place cannot complete in time. This mode began as an alpha feature gate in the kubernetes/autoscaler project (around VPA 1.4), off by default, and enabled via --feature-gates=InPlaceOrRecreate=true. Pin your expectations to the specific VPA release you deploy, since its graduation is on a separate track from core Kubernetes.

Why is my resize stuck in PodResizePending?

The kubelet cannot grant it yet. Check the reason: Infeasible means the resize is impossible on the current node — most often a CPU or memory request larger than the node’s allocatable capacity, and it will not clear until capacity appears or you scale nodes. Deferred means it may become possible later — commonly a memory decrease blocked because live usage still exceeds the proposed limit.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *