Kubernetes In-Place Pod Resize: Vertical Scaling Without Restarts, VPA, cgroups and QoS (2026)
For most of Kubernetes’ history, the CPU and memory of a running container were immutable. If a database replica needed two more cores or a JVM service had been given too little memory, the only lever was to change the pod template and let the controller delete the pod and create a new one. That meant dropped connections, a cold cache, a fresh scheduling decision, and for stateful or batch workloads, real lost work. Kubernetes in-place pod resize removes that constraint: you patch the resources of a live pod and the kubelet rewrites the container’s cgroup limits, often without touching the process at all.
The feature matters now because it is no longer experimental. It started as alpha in v1.27, reached beta in v1.33 and became stable (GA) in v1.35, and the Vertical Pod Autoscaler (VPA) has grown an update mode that uses it. The practical question has shifted from “does it work?” to “which resizes are safe, which restart the container, and what happens when the node says no?”
This tutorial explains the mechanism end to end, with YAML and kubectl you can adapt, and ends with the failure modes that bite in production.
What this covers: the pod resource model and why it was immutable, the resize subresource and state fields, resizePolicy, what the kubelet does to cgroup v2 files, QoS and memory-decrease constraints, the VPA in-place mode, JVM and Go runtime interactions, and a rollout checklist.
Context and Background
Every container in a pod declares resources.requests and resources.limits for CPU and memory. Requests drive scheduling: the scheduler sums requests of pods on a node and refuses to overcommit allocatable capacity. Limits drive enforcement: the container runtime translates them into Linux control group (cgroup) settings, so the kernel throttles CPU or kills the process when it exceeds memory. Before in-place resize, both were fixed in the pod spec at admission and immutable afterwards. The API server rejected updates to them with a validation error.
That immutability was a deliberate simplification. The scheduler, the kubelet, the container runtime and the quota system all assumed a pod’s resource footprint was a constant. Changing it live means every one of those components must agree on what the footprint is at each instant, and the node must never promise resources it cannot deliver. The enhancement proposal behind this work, KEP-1287, spent years resolving exactly that coordination problem. The lifecycle, per the Kubernetes project’s own release posts, is alpha in v1.27, beta in v1.33 and GA in v1.35 (Kubernetes blog: In-Place Pod Resize Graduates to Stable). The official task page now states that the feature has been stable since v1.35, that its feature gate InPlacePodVerticalScaling is locked on, and that setting it explicitly has no effect. A newer, separate effort extends the idea to pod-level resources (spec.resources), which the docs list as beta since v1.36; this article focuses on the per-container feature.
Why did people want this so badly? Three workload families dominate. Stateful services such as databases and caches have expensive warm-up: a recreated pod must re-read data and rebuild buffer pools. Long-running batch and ML jobs have phases with very different needs, for example a data-loading phase that is I/O bound and a compute phase that wants more CPU. And latency-sensitive services with spiky load would rather grow headroom for a few minutes than overprovision all day. The cost angle is just as real: teams that right-size using tools like those discussed in our OpenCost allocation and VPA rightsizing guide can only act on the findings without downtime if resizing is cheap.
In-place resize is not horizontal scaling, and it does not replace it. It changes how big one pod is, within the capacity of the node it already sits on. If the node is full, a resize can be deferred or declared infeasible, and the answer is still “schedule somewhere else”, which is where node-level provisioning such as Karpenter versus Cluster Autoscaler comes in. Managed platforms differ in how quickly they expose new Kubernetes minors and feature behaviours, so check your provider’s version before planning around it; our EKS vs GKE vs AKS comparison covers upgrade cadence.
The three layers of “resources” you must keep apart
The single most useful mental model is that a resized pod has three notions of its resources at once, and the interesting behaviour lives in the gaps between them.
- Desired: what the user asked for, in
spec.containers[].resources. This is now mutable, but only through theresizesubresource for CPU and memory. - Allocated: what the kubelet has admitted and reserved on the node, reported in
status.containerStatuses[].allocatedResources. The scheduler and the kubelet’s own admission accounting trust this value. - Actual: what the container runtime has really configured, reported in
status.containerStatuses[].resources.
Normally all three match. During a resize they briefly differ, and while they differ the scheduler takes the maximum of desired, allocated and actual when it accounts for the pod, so a half-finished resize can never cause the node to be overcommitted. If you remember one design rule from this article, make it that one: the system is conservative by construction.
Resize Architecture: Subresource, Kubelet and Cgroups
Direct answer: A resize is a patch to the pod’s resize subresource. The API server records the new desired resources, the kubelet checks whether the node can fit them, updates the allocated values if so, calls the container runtime to rewrite cgroup limits, and finally reports the new actual values in pod status. Containers restart only if their resizePolicy says so.

Figure 1: The control flow of an in-place resize. The user edits desired state, the kubelet decides, and the runtime rewrites cgroup files.
The figure shows the division of labour. Nothing in the control plane applies the change: the scheduler is not involved in the happy path, and no controller recreates anything. The kubelet is the decision maker because it alone knows the node’s real allocatable capacity and which other pods are running there. This is also why the resize can end in three different ways: applied, deferred or infeasible.
The resize subresource
Resource fields in a running pod spec are not editable through a plain kubectl edit or kubectl patch. Resizes go through the resize subresource, a separate endpoint with its own RBAC verb target (pods/resize). That separation is a security feature: you can let an autoscaler or an operator resize pods without granting it the right to edit anything else in the pod spec. It also gives auditing a clean signal, since every resize is a distinct API request.
A minimal CPU resize looks like this. The command shape matches the example in the Kubernetes documentation and community walk-throughs of the GA release; the container name and values are placeholders, and I have not run this exact snippet against a live cluster, so treat it as a template. kubectl 1.32 or later is needed for the --subresource resize flag.
# Untested template: raise CPU on a running pod without recreating it
kubectl patch pod web-0 --subresource resize --type merge -p '{
"spec": {"containers": [{
"name": "app",
"resources": {
"requests": {"cpu": "800m"},
"limits": {"cpu": "800m"}
}
}]}
}'
After the patch, inspect the three layers. The describe output shows resize conditions, and a JSONPath query shows what the container is really running with:
kubectl describe pod web-0 | grep -A6 Conditions
kubectl get pod web-0 -o jsonpath='{.status.containerStatuses[0].allocatedResources}'
kubectl get pod web-0 -o jsonpath='{.status.containerStatuses[0].resources}'
kubectl get pod web-0 -o jsonpath='{.status.containerStatuses[0].restartCount}'
The restart count is your proof of a hot resize. If it did not move and the process ID inside the container is unchanged, the process never noticed. Community demonstrations of the GA release use exactly this technique, exposing the PID and the contents of /sys/fs/cgroup/cpu.max from inside the container.
Pod conditions and the three outcomes
The kubelet reports progress through two pod conditions. PodResizePending means a resize was requested but has not been applied; its reason distinguishes why. PodResizeInProgress means the kubelet is actively applying the change. The reasons for pending, as documented and as the VPA code treats them, are Deferred and Infeasible.
Deferred means the node does not have room right now but might later, for instance when another pod terminates. The kubelet keeps the resize queued and retries it as capacity frees up. Infeasible means the node can never satisfy the request, for example asking for more CPU than the node has in total, or a change that would alter the pod’s QoS class. An infeasible resize will not succeed by waiting; the pod needs to be moved or the request lowered.
At GA the status also carries observedGeneration fields, which let a controller tell whether the kubelet has processed this change rather than an earlier one. That sounds minor but it fixes a classic race: a controller patches resources, reads status a moment later, sees old values that look “settled”, and wrongly concludes the resize finished. Comparing observed generation against the pod’s generation removes the ambiguity. (The exact field placement is described in the GA docs; confirm the field names against your cluster’s API reference before building automation on them.)
Why the kubelet, not the scheduler, decides
An alternative design would route every resize through the scheduler so that it could pick a new node if the current one is full. The Kubernetes design deliberately did not do this for the stable feature: moving a pod is a recreate, which is exactly what the feature avoids. The consequence is that in-place resize is strictly a node-local operation, and the interesting fallback logic (try in place, otherwise evict and reschedule) must live in a controller above it. Section on VPA below shows that this is precisely how the VPA is built. The project has also signalled further work in this area, including scheduler-side preemption to make room for a resize, which appears as an alpha item in a later release’s blog index; I did not read that post in full, so treat its details as unverified.

Figure 2: The life of a resize request as a state machine. Deferred resizes retry; infeasible ones do not.
Figure 2 is worth keeping in your head when you write alerts. A pod that sits in a pending-deferred state for minutes is a capacity signal. A pod that goes infeasible is a configuration or placement signal. A pod stuck in progress for a long time is usually a runtime or memory-decrease signal, which the next sections explain.
Deeper Analysis: resizePolicy, cgroup v2 and QoS
The architecture explains who decides; this section explains what actually changes on the node and what the application feels. Three mechanisms matter: the per-resource restart policy, the cgroup v2 files the runtime rewrites, and the QoS class that bounds what you may change.
resizePolicy: choosing hot or restart per resource
Each container can declare a resizePolicy list, one entry per resource name, with a restartPolicy of either NotRequired or RestartContainer. NotRequired means the runtime updates the limit on the running container and leaves the process alone. RestartContainer means the kubelet restarts that one container (not the pod) so the new limit is applied from process start. If you omit the field, the default is NotRequired for each resource.
# Template, adapt names and values. Not executed against a live cluster.
apiVersion: v1
kind: Pod
metadata:
name: web-0
spec:
containers:
- name: app
image: registry.example.com/web:1.0
resizePolicy:
- resourceName: cpu
restartPolicy: NotRequired
- resourceName: memory
restartPolicy: RestartContainer
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 500m
memory: 512Mi
The pairing above, CPU hot and memory restart, is the one both the community GA write-ups and common sense recommend. CPU is a rate: if you give a process more of it, the kernel scheduler simply lets it run more, and nothing in the process needs to cooperate. Memory is a quantity the application sizes itself against: heap ceilings, cache budgets and buffer pools are typically decided at startup. Raising a memory limit under a process that still believes it has the old amount gives you no benefit, and lowering it under a process that still uses the old amount gives you an out-of-memory kill. Restarting the container on a memory change forces the application to re-read its environment and size itself again.
Be precise about the cost of RestartContainer. The pod keeps its identity: same UID, same IP, same node, same volumes (an emptyDir survives container restarts). What you lose is the process: in-memory state, open connections and warm caches. The restart count increments, and the process ID changes. It is a much cheaper restart than a pod recreation, but it is still a restart, and you must size readiness probes and termination grace for it. There is also a compatibility rule: a pod whose restartPolicy is Never cannot ask a container to restart on resize, and the VPA documentation calls this out as a precondition for its in-place mode.
| Change | Typical policy | What happens | Risk |
|---|---|---|---|
| CPU increase | NotRequired | cgroup cpu.max and weight rewritten live | Low; threads may need to be able to use extra cores |
| CPU decrease | NotRequired | Throttling begins at new quota | Latency rises if load persists |
| Memory increase | NotRequired or RestartContainer | Limit raised; app may not use it until restart | Wasted headroom if runtime caps heap |
| Memory decrease | RestartContainer preferred | Best effort without restart | OOM kill or stuck resize if usage is above new limit |
What the kubelet writes into cgroup v2
Linux cgroup v2 exposes resource control as files in a unified hierarchy under /sys/fs/cgroup. Kubernetes creates a cgroup for the pod and a child for each container; resize rewrites files in the container’s cgroup, and in the pod’s, since the pod-level cgroup must remain at least as large as the sum of its containers. This explanation of the file mapping is from my knowledge of the kubelet and runtime conventions rather than a quoted passage of the resize docs, so verify the details on your own nodes by reading the files, as shown below.
For CPU there are two knobs. A request becomes a relative weight (cpu.weight in cgroup v2, cpu.shares in v1): it only matters under contention, when the kernel divides CPU time in proportion to weights. A limit becomes a hard quota in cpu.max, written as quota period, for example 50000 100000 for half a core with the default 100 millisecond period. Hitting the quota causes throttling for the remainder of the period, which is why CPU limits can hurt tail latency even on a node with idle cores. The conversion from millicores is linear: a 500m limit is a 50 ms quota per 100 ms period; a 2000m limit is 200 ms, spread across however many threads run.
For memory, the limit becomes memory.max, the hard ceiling that triggers the kernel out-of-memory killer when exceeded. The request influences scheduling and eviction ranking, and on newer setups with the memory QoS feature can influence memory.min and memory.high, but that feature is separate and not on by default in my understanding; I did not re-verify its current status.
You can watch a resize land from inside the container, which is the most convincing demonstration to a sceptical team:
# Inside the container, before and after the patch (illustrative)
cat /sys/fs/cgroup/cpu.max # e.g. "50000 100000" before, "80000 100000" after
cat /sys/fs/cgroup/memory.max # byte value of the memory limit
cat /proc/1/status | grep -i vmrss
This requires cgroup v2 file layouts; on cgroup v1 nodes the file names differ (cpu.cfs_quota_us, memory.limit_in_bytes). Which cgroup version your nodes run is a node image decision, and cgroup v2 is the modern default on current distributions. The kubelet also depends on the container runtime supporting the CRI call that updates container resources; recent containerd and CRI-O releases do, but check your exact versions and the project’s runtime requirements in the official docs before depending on this in production.

Figure 3: How spec values map to cgroup v2 files. Limits become hard quotas, requests become weights and scheduling inputs.
A worked example with illustrative numbers
Suppose a pod runs one container with a 500m CPU limit and 512Mi memory limit on a node whose allocatable CPU is 4000m, with 3200m already allocated to other pods. All numbers here are illustrative, not measured.
A resize to 800m CPU needs 300m extra. Free allocatable is 4000 − 3200 − 500 = 300m, which fits exactly, so the kubelet admits it, updates allocatedResources, and rewrites cpu.max from 50000 100000 to 80000 100000. No restart. A resize to 1000m needs 500m extra but only 300m is free, so the resize is Deferred. If a neighbour pod finishes and frees 400m, the kubelet can retry and succeed. A resize to 5000m exceeds the node’s total allocatable 4000m, so it is Infeasible and will never succeed here.
Notice what the scheduler does during the deferred window: it counts the maximum of desired and allocated for this pod, so it will not place a new pod into capacity this pod is waiting for. That prevents a livelock where the pod waits forever while newcomers consume the room. The flip side is that a large pending resize reserves headroom in the scheduler’s view, which can make a nearly full node look fuller than it is.
QoS: the immutable ceiling
Kubernetes assigns every pod one of three quality-of-service classes at creation, based on its resources: Guaranteed (every container has CPU and memory requests equal to limits), Burstable (at least one request or limit set, but not Guaranteed) and BestEffort (none set). The class drives eviction order under node pressure and shapes the pod’s place in the cgroup hierarchy, and it cannot change after creation. A resize must therefore keep the pod in its class.
The consequences are concrete. A Guaranteed pod can only be resized if requests and limits stay equal for both resources, so you patch both fields together to the same value; changing only the limit would silently turn it Burstable, which is rejected. A Burstable pod can be resized freely within its class, including raising limits without raising requests. A BestEffort pod has nothing to resize, since adding resources would promote it to Burstable. This is the most common reason a team’s first resize attempt fails, and VPA’s in-place mode explicitly falls back to eviction when a recommendation would change the QoS class.
VPA In-Place Mode and Runtime Interactions
In-place resize is a primitive. Nobody wants to run kubectl patch by hand across hundreds of pods, so the interesting consumer is the Vertical Pod Autoscaler. VPA is not part of core Kubernetes; it lives in the kubernetes/autoscaler project and consists of a recommender (computes target requests from usage history), an updater (applies recommendations to running pods) and an admission controller (injects recommendations into new pods). Historically the updater could only apply a new recommendation by evicting the pod, so VPA was disruptive by design, and many teams ran it in recommendation-only mode for that reason.
InPlaceOrRecreate: try the hot path, fall back to eviction
VPA now has an update mode named InPlaceOrRecreate. According to the operational guidance I could retrieve, it reached alpha in VPA 1.4, beta in VPA 1.5 and GA in VPA 1.6, with the VPA-side feature gate enabled by default in 1.5 and removed later; a separate InPlace mode (resize in place only, never evict) was described as alpha behind its own gate in VPA 1.7. These version details come from a third-party operations write-up rather than the VPA release notes, so check the autoscaler repository before pinning versions. The cluster itself must run a Kubernetes version where in-place resize exists, which on any current cluster it does.
The behaviour is a well-defined escalation. The updater sends a resize request through the pods/resize subresource. If the kubelet applies it, nothing else happens and, importantly, the Eviction API is never involved, so PodDisruptionBudgets are not consulted: the pod is not being disrupted. If the attempt fails, VPA falls back to evicting the pod so that the replacement is created with the new requests from the admission controller. Per that same guidance, fallback triggers include:
- the kubelet reports the resize Infeasible, or an error, or an unrecognised resize condition;
- a Deferred resize remains unresolved for more than five minutes;
- an in-progress resize remains unresolved for more than one hour;
- the resize request itself errors; and
- the recommended values would change the pod’s QoS class.
Fallback evictions go through the normal VPA eviction path, so they honour PodDisruptionBudgets and the minimum-replica restrictions. Two operational prerequisites follow. The VPA updater’s service account needs RBAC permission on pods/resize. And each container’s resizePolicy must be compatible with the pod’s restartPolicy; VPA does not override the policy you wrote.
# Template VPA using in-place updates. Verify field names against your VPA version.
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
minAllowed: {cpu: 250m, memory: 256Mi}
maxAllowed: {cpu: "4", memory: 4Gi}
controlledResources: ["cpu", "memory"]

Figure 4: The VPA escalation path. A successful in-place resize bypasses eviction entirely; failures fall back to recreate.
What VPA in-place does and does not fix
It removes the most visible cost of vertical autoscaling: eviction churn. A deployment whose requests drift by 10 percent no longer has pods bouncing. It does not remove the fundamental VPA caveats. VPA still bases recommendations on historical usage percentiles, so it reacts to trends rather than spikes. It should not be combined with an HPA scaling on the same CPU or memory metric, because the two controllers will fight (this is long-standing guidance for VPA). And it does not make memory decreases safe; see the next sections.
A subtle point about requests versus limits: VPA scales requests and, by default, scales limits proportionally to preserve the original ratio. For a Guaranteed pod that ratio is one, so requests and limits move together and QoS is preserved. For a Burstable pod with a large limit-to-request ratio, proportional scaling can produce big absolute limit jumps. Use minAllowed, maxAllowed and, where needed, controlledValues: RequestsOnly so the autoscaler cannot move limits you chose deliberately.
JVM, Go and the runtime that does not know
A cgroup limit change is invisible to a process unless the process looks. This is the central hazard of “no restart”.
The JVM reads container limits at startup to size its heap ergonomics: with defaults, the maximum heap is a fraction of the container’s memory limit (commonly a quarter unless -XX:MaxRAMPercentage is set). The heap maximum is fixed for the life of the JVM. If you raise the memory limit from 2Gi to 4Gi in place, the heap ceiling stays where it was; the extra headroom goes only to off-heap usage such as metaspace, thread stacks and direct buffers. Raising memory without a restart on a JVM therefore delivers almost nothing for the heap, which is exactly why RestartContainer is the sensible memory policy for Java. CPU is subtler: the JVM sizes garbage-collector thread counts and the common fork-join pool from the processor count it observes at startup, so raising the CPU quota will let existing threads run more, but will not add GC workers until restart. Lowering memory in place is dangerous for the opposite reason: the heap may already be larger than the new limit.
The Go runtime sets GOMAXPROCS at startup from the number of CPUs visible to it. Older Go versions looked at the host’s CPU count and ignored the cgroup quota, which caused throttling in containers; newer Go releases are container-aware for CPU limits, but my understanding is that the value is read at start rather than tracking later changes, so confirm against the release notes for your Go version. Memory is similar: the optional GOMEMLIMIT soft limit is fixed when set, typically via an environment variable that does not change on resize. The pragmatic stance for both ecosystems is the same: hot resize is great for CPU quota on workloads that already have enough threads; anything where the runtime sizes itself from the limit wants a container restart.
Node.js, Python and others vary, but the rule generalises. Ask: does this runtime read the limit once? If yes, in-place without restart changes what the kernel enforces but not what the application believes. Mismatch between belief and enforcement is the root of most in-place resize incidents.
Trade-offs, Gotchas, and What Goes Wrong
Memory decreases are best effort. Lowering a CPU limit is always safe for the kernel: it just throttles sooner. Lowering a memory limit below current usage would trigger the OOM killer if applied blindly. The community GA description says memory decrease is best effort without restart because the application must free memory. My understanding of the design is that the kubelet compares current usage to the new limit and does not apply a decrease that usage already exceeds, leaving the resize in progress until usage drops or the request is changed. Because I could not confirm the exact check from the docs I was able to read, test this behaviour on your own cluster before relying on it. The safe operational pattern for decreases is RestartContainer, or accept recreating the pod via a rollout.
Quota and admission side effects. ResourceQuota and LimitRange apply to resize requests as they do to creation; a resize that would push a namespace over quota is rejected, and a request outside LimitRange min/max is invalid. Automation that resizes many pods at once can hit quota in the middle of a fleet-wide change and leave half the pods updated.
Not everything is resizable. Per the GA description: init containers and ephemeral containers cannot be resized, sidecar (restartable init) containers can, and Windows pods are not supported. Only CPU and memory are resizable; extended resources such as GPUs are not. Node-level CPU and memory managers (for example static CPU manager policy that pins exclusive cores to Guaranteed pods) may block changes, since changing a pinned core count means re-pinning. If your latency-critical pods use the static policy, test resize explicitly.
Drift between spec and the controller’s template. A resize changes the live pod, not the Deployment or StatefulSet that owns it. The next rollout, or any pod replacement, brings back the template values. Teams that resize by hand then see a “mysterious” revert at the next deploy. Either have VPA own the requests, or update the workload template after a manual resize, ideally through GitOps so the cluster and repository agree.
Observability gaps. Dashboards that group by the pod’s requests will show a step change mid-life; cost tools that sample requests once per day will mis-attribute. Alert on resizes stuck in Deferred or in progress, and keep the audit log for pods/resize calls as your change history.
Autoscaler interactions. A node autoscaler decides node count from pending pods and requests. A Deferred resize is not a pending pod, so the node autoscaler may never see the demand. If the VPA falls back to eviction after five minutes, then a pending replacement finally triggers scale-up, which is slow. For latency-sensitive services, leave headroom on nodes or size by bin-packing policy rather than assuming the cluster will react to a deferred resize.
| Symptom | Likely cause | First check |
|---|---|---|
| Patch rejected on spec | Used plain patch, not the resize subresource | Add --subresource resize, kubectl 1.32 or newer |
| Resize rejected, QoS error | Request would change QoS class | Keep requests equal to limits for Guaranteed pods |
| Pending reason Deferred | Node lacks free allocatable | Check node allocated vs allocatable |
| Pending reason Infeasible | Exceeds node total or policy forbids | Move pod or lower request |
| Container restarted unexpectedly | resizePolicy RestartContainer | Review per-resource policy |
| JVM ignores added memory | Heap ceiling fixed at startup | Restart or set MaxRAMPercentage and restart |
| Resize stuck in progress | Memory decrease below usage | Reduce usage or restart container |
| Reverts after deploy | Template not updated | Update owner object or let VPA manage |
Practical Recommendations
Adopt in-place resize deliberately rather than universally. Start with the workloads where the economics are obvious: stateful services with long warm-up, and services behind a VPA that currently churn pods. Leave stateless, fast-starting, horizontally scaled services on horizontal scaling; their restart is already cheap, and in-place adds states you must monitor.
Set resizePolicy explicitly rather than relying on defaults. A reasonable baseline is CPU NotRequired and memory RestartContainer, then loosen memory to NotRequired only for runtimes you have verified will use extra memory without a restart. Prefer Guaranteed QoS for pods you intend to resize, because the rule “requests equal limits, move them together” is easy to automate and impossible to violate by accident. Keep node headroom: a resize can only succeed if the node has room, so a bin-packed cluster turns every resize into a Deferred and then an eviction.
Use VPA in Off mode first to see what it would recommend, then InPlaceOrRecreate once you have confirmed your RBAC, runtime and PodDisruptionBudgets. Bound it with minAllowed and maxAllowed. Make sure your cluster version, container runtime and (on managed platforms) the provider’s feature exposure are all current.
Rollout checklist:
- Confirm the control plane and kubelets run a release where the feature is stable (v1.35 or later), and the runtime supports updating container resources.
- Confirm nodes use cgroup v2 and test a resize on a staging node, reading
cpu.maxandmemory.maxbefore and after. - Grant
pods/resizeRBAC only to the controllers that need it. - Write explicit
resizePolicyper container and per resource. - Verify each runtime’s behaviour for CPU and memory changes without restart.
- Alert on pods whose resize stays Deferred or in progress beyond a few minutes.
- Reconcile the owning Deployment or StatefulSet template with live values.
- Keep PodDisruptionBudgets and replica counts sane for VPA’s fallback evictions.
Frequently Asked Questions
Is Kubernetes in-place pod resize stable?
Yes. The feature (KEP-1287, also called in-place pod vertical scaling) was alpha in v1.27, beta in v1.33 and went stable in v1.35, according to the Kubernetes project’s release post. The InPlacePodVerticalScaling feature gate is now locked on, so you cannot disable it. Pod-level resource resize is a separate, newer feature that the documentation lists as beta in v1.36. Managed services may lag the upstream release, so check your provider’s version.
Does resizing a pod restart it?
Not necessarily. Each container’s resizePolicy decides per resource. With NotRequired, the runtime rewrites the cgroup limits and the process keeps running; with RestartContainer, only that container restarts, not the pod. The pod keeps its UID, IP and node either way. Check restartCount and the container’s process ID to see whether a restart happened. A pod is never recreated by a resize.
How do I resize a pod with kubectl?
Patch the resize subresource: kubectl patch pod NAME --subresource resize --type merge -p '{"spec":{"containers":[{"name":"app","resources":{"requests":{"cpu":"800m"},"limits":{"cpu":"800m"}}}]}}'. You need kubectl 1.32 or newer and RBAC on pods/resize. A plain patch of the spec is rejected because container resources outside this path remain immutable.
Why was my resize Deferred or Infeasible?
Deferred means the node lacks free allocatable capacity now but the kubelet will retry when capacity frees up. Infeasible means it cannot work on this node, such as exceeding total allocatable capacity or altering the pod’s QoS class. Inspect the PodResizePending condition and its reason, then compare the node’s allocated resources to its allocatable. Infeasible requests need a lower value or a move to another node.
Can I change a pod’s QoS class by resizing?
No. QoS (Guaranteed, Burstable, BestEffort) is fixed at creation, and a resize that would change it is refused. For a Guaranteed pod, change requests and limits together so they stay equal. BestEffort pods have no requests or limits to adjust and cannot be promoted in place. VPA’s in-place mode falls back to eviction when its recommendation would cross a class boundary.
How does the VPA use in-place resize?
In InPlaceOrRecreate mode the updater first tries a resize via pods/resize. If the kubelet applies it, no eviction happens and PodDisruptionBudgets are not consulted. If the resize is infeasible, errors, stays deferred past about five minutes or in progress past about an hour, or would change QoS, VPA evicts the pod so the replacement gets the new values. Feature maturity depends on your VPA version.
Further Reading
- Managed Kubernetes compared: EKS vs GKE vs AKS networking, upgrades, autoscaling and cost for how quickly each provider exposes new Kubernetes releases.
- OpenCost allocation, VPA rightsizing, showback and chargeback for turning resize into measurable savings.
- Karpenter vs Cluster Autoscaler for GPU workloads for the node-level fallback when a resize cannot fit.
- Kubernetes blog: In-Place Pod Resize Graduates to Stable (primary source).
- Kubernetes docs: Resize CPU and Memory Resources assigned to Containers and Vertical Pod Autoscaling.
By Riju — about
