Kubernetes In-Place Pod Resize: Scale a Pod Vertically Without Restarting It

Yes — Kubernetes can change the CPU and memory of a running pod without deleting it and, in most cases, without restarting its containers. The feature is called In-Place Pod Resize (the KEP calls it In-Place Pod Vertical Scaling), it has been stable since Kubernetes 1.35, and on any cluster at 1.33 or later it is already on by default. You trigger it by patching the pod’s resize subresource:

kubectl patch pod checkout-7d9f8c6b5-x2lqp --subresource resize --patch \
  '{"spec":{"containers":[{"name":"app","resources":{"requests":{"cpu":"1"},"limits":{"cpu":"2"}}}]}}'

The kubelet updates the container’s cgroup, the pod keeps its name, IP, node and in-memory state, and restartCount stays where it was. That is the short answer. The long answer — which this article is about — is that “without restarting” comes with conditions: which resource you are changing, what resizePolicy says, whether the node has room, what QoS class the pod was born with, and whether the runtime inside the container can actually use the new numbers. And the moment you want this to happen automatically, you are in Vertical Pod Autoscaler territory, where the new InPlaceOrRecreate and InPlace modes finally change the old “VPA means evictions” trade-off — and where running HPA and VPA together still needs care.

If you need the fundamentals first, the VPA introduction covers when vertical scaling makes sense at all, and Kubernetes resource requests and limits covers what the numbers you are about to change actually do.

Why Resource Changes Used to Mean a Restart

For most of Kubernetes’ history, spec.containers[*].resources was immutable on a Pod. Requests and limits were decided at admission, the scheduler placed the pod against them, the kubelet wrote them into the container’s cgroup, and that was it for the life of the pod. If you wanted more CPU, you changed the Deployment template, the Deployment controller created a new ReplicaSet, and the old pods were terminated one by one.

That is fine for a stateless web tier with twenty replicas and a fast readiness probe. It is painful for:

  • stateful workloads where a restart means cache warm-up or a replica resync;
  • long-running batch or ML jobs where a restart throws away hours of progress;
  • JVM services that need a lot of CPU for thirty seconds of startup and very little afterwards.

It is also why the Vertical Pod Autoscaler had a reputation problem. Its only way to apply a new recommendation to a running pod was to evict it and let the admission webhook stamp the new values on the replacement. Many teams ran VPA in recommendation-only mode forever because they did not want an autoscaler that deletes pods.

Feature History: From Alpha to Stable

In-Place Pod Resize took more than six years from conception to GA. The version history matters because behavior changed along the way, and a lot of blog posts still describe the alpha or beta semantics.

Kubernetes versionStateWhat changed
1.27AlphaInPlacePodVerticalScaling feature gate, off by default. Resources became mutable directly on spec, progress reported in status.resize.
1.33Beta, on by defaultNew resize subresource, status.resize replaced by the PodResizePending and PodResizeInProgress conditions.
1.35 (Dec 2025)Stable, locked onMemory limit decreases allowed (best-effort OOM protection), prioritized retry of deferred resizes, new kubelet metrics and pod events. Pod-level resources resize added as alpha.
1.36—In-place resize of pod-level resources (spec.resources) promoted to beta, on by default.
1.37—Alpha: scheduler preemption for deferred resizes (InPlacePodVerticalScalingSchedulerPreemption) and resize of memory-backed emptyDir size limits (InPlacePodVerticalScalingMemoryBackedVolumes, cgroup v2 only).

On 1.35+ the feature cannot be turned off, and your kubectl client needs to be 1.32 or newer to use --subresource resize; older clients fail with an “invalid subresource” error even against a new API server.

How In-Place Resize Works

Three fields carry the whole mechanism, and reading them correctly saves most of the debugging time.

spec.containers[*].resources is the desired state. For CPU and memory, these fields are now mutable — but only through the resize subresource. A normal kubectl edit pod or kubectl apply against the main pod resource is still rejected for resource changes.

status.containerStatuses[*].resources is the actual state. It reflects what the kubelet has really configured in the container’s cgroup. When desired and actual differ, a resize is pending or in progress.

status.containerStatuses[*].allocatedResources is what the kubelet has admitted. It is mostly for scheduling. While a resize is incomplete, the scheduler accounts for the pod using the maximum of desired, allocated and actual requests, so a node never gets overcommitted in the gap.

The flow is:

  1. You (or an autoscaler) patch the pod’s resize subresource with new requests and limits.
  2. The API server validates the change — QoS class preserved, only CPU and memory touched, no resource removed.
  3. The kubelet on the pod’s node checks whether the new requests fit in the node’s allocatable capacity.
  4. If they fit, the kubelet admits the resize, updates the cgroup through the container runtime (containerd or CRI-O via the CRI UpdateContainerResources call), and — depending on resizePolicy — restarts the container or not.
  5. When the runtime confirms, status.containerStatuses[*].resources matches the spec and the conditions clear.

A Complete Example

Create a pod with an explicit resize policy:

apiVersion: v1
kind: Pod
metadata:
  name: resize-demo
  namespace: resize-lab
spec:
  containers:
    - name: app
      image: registry.k8s.io/pause:3.9
      resizePolicy:
        - resourceName: cpu
          restartPolicy: NotRequired      # default, explicit here
        - resourceName: memory
          restartPolicy: RestartContainer
      resources:
        requests:
          cpu: 500m
          memory: 256Mi
        limits:
          cpu: "1"
          memory: 512Mi
kubectl create namespace resize-lab
kubectl apply -f resize-demo.yaml

This pod is Burstable (requests differ from limits). Now double the CPU:

kubectl -n resize-lab patch pod resize-demo --subresource resize --patch \
  '{"spec":{"containers":[{"name":"app","resources":{"requests":{"cpu":"1"},"limits":{"cpu":"2"}}}]}}'

Check desired versus actual, and the restart count:

kubectl -n resize-lab get pod resize-demo \
  -o jsonpath='{.spec.containers[0].resources}{"\n"}{.status.containerStatuses[0].resources}{"\n"}{.status.containerStatuses[0].restartCount}{"\n"}'

Both resource blocks should show cpu: 1 / cpu: 2, and restartCount is still 0. Now change the memory limit:

kubectl -n resize-lab patch pod resize-demo --subresource resize --patch \
  '{"spec":{"containers":[{"name":"app","resources":{"limits":{"memory":"768Mi"}}}]}}'

This time restartCount goes to 1, because the memory policy is RestartContainer. The pod is the same pod — same UID, same IP, same node — but the container process was restarted to pick up the new value.

resizePolicy: NotRequired vs RestartContainer

resizePolicy is a per-container, per-resource setting with two values:

  • NotRequired (the default when unset): apply the new value to the running container’s cgroup without restarting it.
  • RestartContainer: restart the container so the process starts with the new value.

The rule when several resources change at once is simple: if any changed resource has RestartContainer, the container restarts. With the policy above, a CPU-only change is in place, a memory-only change restarts, and a CPU-and-memory change restarts.

One restriction: if the pod’s overall restartPolicy is Never (typical for Jobs that must not rerun a container), every resizePolicy entry must be NotRequired. You cannot declare a resize that would require a restart the pod is not allowed to do.

Which policy should you choose?

CPU: almost always NotRequired. Changing CPU requests and limits changes CFS shares and quota in the cgroup. The kernel applies it on the next scheduling period and the process does not need to know. The one caveat is runtimes that size thread pools from the CPU count at startup — the JVM’s GC threads and ForkJoinPool.commonPool() are the classic example. The process gets more CPU time immediately, but it will not create more worker threads. (Go is the pleasant exception: since Go 1.25 the runtime reads the cgroup CPU limit and adjusts GOMAXPROCS while running, unless you pinned it with an environment variable.) For most services that is fine; for heavily parallel compute, a restart may still be worth it.

Memory: depends on whether the process can use it. Raising a cgroup memory limit is harmless, but a JVM started with -XX:MaxRAMPercentage=75 computed its max heap from the old limit and will not grow the heap past it. Python and most interpreted runtimes do not re-read limits either — the Kubernetes GA announcement itself notes that Java and Python do not support resizing memory without a restart. For these, RestartContainer on memory is the honest setting: you still avoid rescheduling and keep the pod’s identity, you just restart the process. Go with GOMEMLIMIT set from the downward API, or native services that allocate on demand, can use NotRequired.

Memory Decreases: Allowed, but Best-Effort

Until 1.35, decreasing a container’s memory limit in place was simply forbidden. The GA release lifted that restriction with a safeguard: when the memory restartPolicy is NotRequired, the kubelet checks current memory usage before applying a lower limit. If usage is already above the new limit, the resize is not applied and stays in progress rather than triggering an immediate OOM kill.

The check is explicitly best-effort. There is a race: usage can spike between the check and the cgroup write, and then the kernel OOM-kills the container. Treat memory decreases on NotRequired containers as something you do with margin — shrink to well above the observed working set, not to it. Moving the check into the container runtime to close that race is listed as future work.

Watching a Resize: Conditions, Deferred and Infeasible

When the kubelet cannot apply a resize immediately, it tells you through pod conditions.

PodResizePending means the kubelet has not admitted the resize. The reason field says why:

  • Infeasible: the request can never fit on this node — for example, asking for 16 CPUs on an 8-CPU node, or a change the node’s configuration forbids. It will not be retried until you change the request.
  • Deferred: the request does not fit right now, but could if other pods leave. The kubelet retries automatically.

PodResizeInProgress means the kubelet has admitted the resize and allocated the resources but the runtime has not finished applying them. It is usually brief. If actuation fails, the condition carries reason: Error and a message.

To see them:

kubectl -n resize-lab get pod resize-demo \
  -o jsonpath='{range .status.conditions[?(@.type=="PodResizePending")]}{.reason}{": "}{.message}{"\n"}{end}'

kubectl -n resize-lab describe pod resize-demo | sed -n '/Conditions:/,/Volumes:/p'

Since 1.35 the pod also emits dedicated events for resizes, and status.observedGeneration tells you which version of the spec the kubelet has acknowledged — useful when several resizes are issued in a row and you need to know which one the status refers to.

When several resizes on a node are Deferred, they are retried in priority order: higher PriorityClass first, then Guaranteed before Burstable, then the longest-waiting first. A deferred high-priority resize does not block lower-priority ones that happen to fit. And with the 1.37 alpha InPlacePodVerticalScalingSchedulerPreemption gate, the scheduler can go further and evict lower-priority pods to make room for a higher-priority pod’s deferred resize — the same preemption semantics as scheduling, applied to growth.

Limitations You Will Hit

As of Kubernetes 1.37, the official list of restrictions is:

LimitationWhat it means in practice
Only CPU and memoryEphemeral storage, GPUs, hugepages and other extended resources are still immutable.
QoS class is fixedThe QoS class decided at creation must survive the resize. Guaranteed pods must keep requests equal to limits; Burstable pods cannot make requests equal limits for both CPU and memory; BestEffort pods cannot gain requests or limits.
No removalOnce a request or limit is set, you can change it but not delete it.
Container typesRegular containers and sidecars (restartable init containers) can be resized; classic init containers and ephemeral debug containers cannot.
WindowsWindows pods do not support in-place resize.
Static CPU / Memory ManagerPods pinned by the kubelet’s static CPU manager or static memory manager policies cannot be resized in place.
SwapPods using swap can only change memory requests if the memory resize policy is RestartContainer.
Memory decreaseBest-effort OOM protection, as described above.

The other restriction that is not in the table but matters more than all of them: in-place resize is a Pod operation, not a Deployment operation. If you change resources in a Deployment, StatefulSet or DaemonSet template, the controller still performs a normal rolling update with new pods. Workload controllers do not propagate template changes in place. Resizing in place means patching individual pods — by hand, with a script, or with an autoscaler that does it for you. And whatever you patch onto a pod is lost the next time the controller replaces it, because new pods are created from the template.

Pod-Level Resources Resize

Kubernetes also supports resources at the pod level — spec.resources as an aggregate budget shared by all containers — and since 1.36 those can be resized in place too (beta, on by default; alpha in 1.35 under InPlacePodLevelResourcesVerticalScaling). The same resize subresource, conditions and retry logic apply:

kubectl patch pod pod-level-demo --subresource resize --patch \
  '{"spec":{"resources":{"requests":{"cpu":"300m"},"limits":{"cpu":"300m"}}}}'

Two rules are specific to pod-level resize. The new pod-level requests must be at least the sum of the container requests, and no individual container limit may exceed the new pod-level limit. And there is no pod-level resizePolicy: the pod cgroup is always changed in place, but containers without their own limit inherit the pod limit as their effective ceiling, so their container-level resizePolicy still decides whether they restart. A container with restartPolicy: RestartContainer for CPU and no CPU limit will restart when you raise the pod’s CPU limit.

Automating It: VPA InPlaceOrRecreate and InPlace

Manual resizes are useful for incidents and experiments. The production use of this feature is the Vertical Pod Autoscaler, which as of VPA 1.8 has six update modes:

updateModeBehaviorStatus
OffRecommendations only, never touches podsStable
InitialApplies recommendations only when pods are createdStable
RecreateEvicts pods to apply recommendations (the default)Stable
AutoSame as RecreateDeprecated — use an explicit mode
InPlaceOrRecreateTries an in-place resize; falls back to evictionAlpha in VPA 1.4, beta in 1.5, GA in VPA 1.6
InPlaceIn-place only; never evicts, retries later insteadAlpha in VPA 1.7 (feature gate InPlace)

InPlaceOrRecreate is the mode most teams should move to:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: checkout
  namespace: shop
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: checkout
  updatePolicy:
    updateMode: InPlaceOrRecreate
  resourcePolicy:
    containerPolicies:
      - containerName: app
        controlledResources: ["cpu", "memory"]
        minAllowed:
          cpu: 250m
          memory: 256Mi
        maxAllowed:
          cpu: "4"
          memory: 4Gi

The VPA updater attempts an in-place update when the pod’s requests fall outside the recommended bounds, after a quick OOM, or for pods running more than 12 hours whose recommendation differs by more than 10%. It falls back to eviction when the resize is Infeasible, has been Deferred for more than 5 minutes, has been in progress for more than an hour, would change the pod’s QoS class, or requires a memory limit decrease the policy does not allow. All containers of a pod are updated together.

A few operational details worth knowing:

  • In-place updates still respect VPA’s eviction tolerance and minReplicas by default. If every container uses NotRequired for both resources, the updater flag --in-place-skip-disruption-budget lets VPA skip those checks for updates that genuinely do not restart anything.
  • The updater exports vpa_updater_in_place_updated_pods_total and vpa_updater_failed_in_place_update_attempts_total, among others. Alert on the failure counter — a steady stream of failed in-place attempts usually means nodes are too full and you are silently back to evictions.
  • InPlace mode is for workloads where any eviction is unacceptable. The price is that a resize that never fits never happens; VPA tracks infeasible attempts and only retries when the recommendation drops below what was already rejected.

CPU Startup Boost: The JVM Use Case

The most compelling use of in-place resize is the startup spike. A Spring Boot or Quarkus-on-JVM service can burn two or three cores for the first 30–60 seconds (class loading, JIT compilation, connection pool warm-up) and then settle at 200 millicores. Sizing requests for the startup means paying for idle cores all day; sizing for steady state means slow starts, failing startup probes and an HPA that adds pods too late to help.

VPA 1.7 introduced CPU Startup Boost (alpha, CPUStartupBoost feature gate), built directly on in-place resize:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: orders
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: orders
  updatePolicy:
    updateMode: InPlaceOrRecreate
  startupBoost:
    cpu:
      type: Factor
      factor: 3
      durationSeconds: 60

The admission controller multiplies the CPU of new pods by the factor, and once the pod is Ready and durationSeconds has elapsed, the updater scales CPU back down in place — to the VPA recommendation if VPA controls the container, otherwise to the original spec. VPA 1.8 added reactive unboosting on top. Boost can also be set per container under containerPolicies, or as a fixed Quantity instead of a factor.

Remember the caveat from the resizePolicy section: the JVM sizes GC and common pool threads at startup, so it keeps the thread counts it chose during the boost — usually a benefit, not a problem.

Using HPA and VPA Together Without Conflicts

“Can I use HPA and VPA together?” is the other question people search for, and in-place resize changes the answer only partially.

The classic conflict: an HPA with a CPU or memory utilization target computes utilization as usage divided by requests. VPA changes requests. When VPA raises the CPU request, utilization drops, and the HPA scales in; with fewer pods, usage per pod rises, VPA raises requests again. The two controllers fight over the same signal from opposite directions. In-place resize makes that loop faster, not safer, because VPA no longer has to wait for evictions to take effect.

The rule remains: never let HPA and VPA act on the same resource metric. The patterns that work:

HPA on a custom or external metric, VPA on resources. Scale out on requests per second, queue depth or consumer lag (via the custom metrics API, Prometheus Adapter or KEDA), and let VPA InPlaceOrRecreate right-size CPU and memory per pod. Each controller owns a different dimension. This is the pattern I recommend for most services that need both.

HPA on CPU, VPA on memory only. Set controlledResources: ["memory"] in the VPA’s container policy. HPA owns CPU-driven horizontal scale, VPA only sizes memory. Works well for services whose memory footprint depends on configuration and data size rather than load.

HPA on CPU, VPA in Off mode. Use VPA purely as a recommendation engine and feed its numbers into the Deployment template through your GitOps pipeline. No live conflict, and you still get data-driven requests.

If your HPA scales on CPU and you are also tuning its behavior, the HPA best practices guide and the HPA memory examples cover the scaling policies. For HPA on event-driven metrics, see KEDA and event-driven autoscaling.

Interaction With Cluster Autoscaling

In-place resize never moves a pod. If a node is full, a resize goes Deferred, and neither Cluster Autoscaler nor Karpenter will add a node for it: they react to unschedulable pods, and a pod with a deferred resize is scheduled and running. The pod stays at its old size until something on the node shrinks or leaves.

That is why InPlaceOrRecreate falls back to eviction after five minutes deferred: an evicted pod with larger requests becomes pending, and pending pods do trigger node provisioning. If you run VPA in pure InPlace mode, plan headroom on nodes explicitly, or accept that some resizes will wait. The 1.37 preemption alpha helps high-priority pods but does not create capacity. For how node autoscalers make those decisions, see Cluster Autoscaler vs Karpenter, and if you are adopting several of these newer features at once, Kubernetes features that hurt in production is a useful sanity check on rollout order.

Frequently Asked Questions

Can Kubernetes scale a pod vertically without restarting the pod?

Yes. Since Kubernetes 1.33 (beta, on by default) and as a stable feature since 1.35, you can change a running pod’s CPU and memory requests and limits through the pod’s resize subresource, for example with kubectl patch pod <name> --subresource resize. The pod keeps its UID, IP and node. Whether the container process restarts depends on the container’s resizePolicy: with NotRequired, the default, the new values are applied to the cgroup live; with RestartContainer, the container is restarted inside the same pod.

Which Kubernetes version made in-place pod resize GA?

Kubernetes 1.35, released in December 2025. The feature was alpha in 1.27 behind the InPlacePodVerticalScaling feature gate and beta, enabled by default, in 1.33. In 1.35 the gate is locked on, memory limit decreases became allowed, and deferred resizes are retried by priority. Resizing pod-level resources is beta since 1.36.

Why does my pod show PodResizePending with reason Deferred?

The node does not currently have enough allocatable CPU or memory for the new requests. The kubelet will retry automatically when capacity frees up, prioritizing by PriorityClass, then QoS class, then waiting time. In-place resize never moves the pod to another node, and cluster autoscalers do not add nodes for deferred resizes, so either free capacity on that node or recreate the pod so the scheduler can place it elsewhere.

Can I change a pod’s QoS class with an in-place resize?

No. The QoS class is fixed at pod creation. A Guaranteed pod must keep requests equal to limits for CPU and memory, a Burstable pod cannot make them equal for both resources, and a BestEffort pod cannot gain requests or limits. Resizes that would change the QoS class are rejected by the API server, and VPA’s InPlaceOrRecreate mode falls back to evicting the pod in that case.

Does changing resources in a Deployment resize its pods in place?

No. Changing resources in a Deployment, StatefulSet or DaemonSet template still triggers a normal rolling update with new pods. In-place resize works on individual pods through the resize subresource. To resize a Deployment’s pods in place automatically, use the Vertical Pod Autoscaler with updateMode: InPlaceOrRecreate or InPlace, and keep in mind that new pods created by the controller always start from the template values.

Can I use HPA and VPA together?

Yes, as long as they do not act on the same resource metric. An HPA with a CPU or memory utilization target divides usage by requests, and VPA changes requests, so the two controllers fight. Safe combinations are HPA on a custom or external metric such as requests per second or queue depth with VPA managing CPU and memory, HPA on CPU with VPA controlling only memory, or VPA in Off mode as a recommendation source for an HPA-managed workload.

Does in-place resize work for JVM applications?

For CPU, yes: the JVM receives more or less CPU time immediately, although thread pools and GC threads sized at startup keep their original counts. For memory, the JVM computes its maximum heap at startup, so raising the limit in place does not give the heap more room. Use RestartContainer for memory on JVM workloads, and consider VPA’s CPU Startup Boost to give JVMs extra CPU during startup and scale back down in place once they are ready.

Conclusion

In-place pod resize turns CPU and memory from birth certificates into dials. On Kubernetes 1.35+ you can grow or shrink a running pod through the resize subresource, and for CPU that genuinely means no restart. Memory is more nuanced: the cgroup changes live, but many runtimes only read their memory budget at startup, so RestartContainer is often the right policy — still far cheaper than rescheduling.

The bigger shift is for autoscaling. VPA’s InPlaceOrRecreate mode, GA since VPA 1.6, removes the main reason teams kept VPA in recommendation-only mode, and CPU Startup Boost finally gives JVM services a clean answer to the startup spike. What has not changed is the rule for combining autoscalers: give HPA and VPA different signals, keep the QoS class in mind from day one, and remember that anything you patch onto a pod lives only until the next rollout.