Skip to main content

Kubernetes 1.35 Takes In-Place Pod Resize to GA: What the Restart-Based Workaround It Just Killed Was Actually Costing You

8 min readDora NodaDora Noda
Share
On this page

A tenant's app on your platform spikes to 3x its normal traffic. Before December 2025, the only way Kubernetes could give that pod more CPU was to kill it and start a new one somewhere else — dropping every open connection, cold-starting the container, and hoping the scheduler found room fast enough to matter. As of Kubernetes v1.35, that's no longer true: in-place pod resize graduated to General Availability, and a running container's CPU and memory requests/limits can now change without a restart at all.

That's the feature. The number that actually matters for a self-hosted platform is what it replaces: a resize that used to mean full pod churn — evict, reschedule, pull, start, pass readiness — now completes as a kubectl patch against the pod's own /resize subresource, applied by the kubelet in single-digit seconds. If your platform's autoscaling logic still evicts and reschedules to change a pod's size, you're paying a bill Kubernetes stopped charging.

The Workaround This Makes Obsolete

Before v1.35, a Kubernetes pod's resources.requests/limits were immutable after creation — the field was part of the pod's spec, and specs (other than a small allowlist like the image tag) don't change after the API server admits them. Every "vertical scaling" tool built on Kubernetes — the Vertical Pod Autoscaler included — had exactly one lever: delete the pod, recreate it with new numbers, let the scheduler place it again.

Here's what that lever actually cost, broken into the steps it took:

StepRestart-based resize (pre-GA)In-place resize (GA, v1.35+)
Trigger a changeDelete pod / patch Deployment → controller recreatesPATCH the pod's /resize subresource
Scheduling decisionNew scheduling round, subject to whatever else changed on the node sinceNone — same pod, same node
Image pullSkipped if image is cached on the target node, 30s–2min+ if not (and there's no guarantee the new pod lands on a node that has it cached)N/A — container isn't recreated
Container start + readiness probeSeveral seconds to tens of seconds, workload-dependentN/A
Connections / in-flight requestsDropped — client has to retry against a new podNot interrupted
Typical totalTens of seconds to a couple of minutes, dominated by whether the image was already warm on the landing nodeSingle-digit seconds — the kubelet's own kubelet_pod_resize_duration_seconds metric tracks this directly

The gap isn't a rounding error. It's the difference between a stateful or singleton workload absorbing a traffic spike transparently and that same workload dropping every open connection to do it — and it's why, six-plus years after the KEP was first filed against kubernetes/enhancements in November 2018, this shipped as one of the most-requested features in the project's history. The implementation took a slower path than the idea: alpha in v1.27 (April 2023), beta in v1.33 (April 2025), stable in v1.35 (December 17, 2025).

How It Actually Works: the Resize Subresource, resizePolicy, and What GA Changed

The mechanism is a new subresource, not a new field. A pod's spec.containers[].resources stays where it's always been; what changed is that Kubernetes now exposes a /resize subresource you can PATCH directly, and the kubelet applies the new values to the running container's cgroup instead of requiring a new container.

Two things gate how "in-place" a given resize actually is:

  • Per-resource resizePolicy. Each container can independently declare NotRequired (the default — apply without a restart) or RestartContainer for CPU and for memory. A container that's fine resizing CPU live but needs a restart to grow its memory limit (some JVM or native-heap workloads fall into this bucket) sets RestartContainer for memory only and NotRequired for CPU — and if you resize both at once, the restart policy for either resource forces a restart of the whole container, so mixing policies doesn't buy you a partial restart.
  • QoS class is immutable across a resize. You can move a pod's requests and limits around, but you can't change whether it's Guaranteed, Burstable, or BestEffort — a Guaranteed pod's requests and limits still have to match after the resize, same as before.

In practice, triggering a resize is a single kubectl patch against the subresource, no different in shape from any other patch call:

bash
kubectl patch pod tenant-app-7d9f8 --subresource resize --patch \
  '{"spec":{"containers":[{"name":"app","resources":{"requests":{"cpu":"1500m"},"limits":{"cpu":"1500m"}}}]}}'

The kubelet picks that up, checks the container's resizePolicy for the resources being touched, and either updates the cgroup in place or restarts the container — the caller doesn't need to know which happened up front; it shows up in kubectl get pod -o yaml under status.resize and in the pod's events.

The one concrete behavior change GA introduced over beta: decreasing a memory limit is now allowed. In beta, only memory limit increases were permitted in place, because the kubelet couldn't guarantee a decrease wouldn't OOM-kill the container mid-shrink; GA ships a best-effort path for decreases (no hard guarantee against an OOM kill if you shrink too aggressively, but no longer a flat prohibition) plus new kubelet metrics and pod events specifically for tracking resize attempts, so a platform can actually observe when a shrink got deferred or failed instead of finding out from a tenant's page.

What Still Needs a Fallback: PodResizePending

The GA feature is not a blank check. If a node doesn't have the spare CPU/memory to satisfy a resize request right now, the resize doesn't fail — it goes into a PodResizePending (Deferred) state and waits for capacity to free up on that same node, with no guaranteed time bound. A pod asking for more than any node in the cluster could ever give it sits deferred forever unless something else intervenes.

That "something else" is still the horizontal path: a Cluster Autoscaler (or, on a Cluster-API-provider-Hetzner fleet, CAPH provisioning a new Hetzner machine and joining it to the pool) has to notice the deferred resize, add capacity, and let the kubelet retry. In-place resize collapses the common case — there's headroom on the node the pod is already running on — down to seconds. It doesn't remove the rare case where there genuinely isn't room, and a platform that treats GA resize as "vertical scaling now always works in place" will get paged the first time a tenant's pod asks for more than the node pool currently has.

Building Autoscaling Logic On the Primitive, Not Around It

This is the part that actually matters for a Cluster-API-managed fleet's own control plane, not just for kubectl users. If your platform's tenant-resource-management logic — the code that watches a tenant's traffic and decides to give their pod more CPU — still works by deleting and recreating pods to change their size, GA in-place resize is a straight replacement, and the reference implementation already exists: the Vertical Pod Autoscaler's 1.4+ InPlaceOrRecreate update mode, built directly on the /resize subresource. VPA in this mode tries the in-place path first and only falls back to the old evict-and-recreate behavior when the in-place attempt can't be satisfied — which is exactly the two-tier logic above (resize-in-place → PodResizePending → horizontal fallback), already shipped and battle-tested by SIG Autoscaling rather than something a platform team has to design from scratch.

Concretely, a self-hosted PaaS's own reconciler for "tenant app needs more resources" should now look like this, in order:

  1. PATCH the pod's /resize subresource with the new CPU/memory values — no eviction, no new scheduling decision.
  2. Watch the resize status and pod events the GA release added (rather than polling resources.requests and guessing) to detect a completed resize versus a Deferred one.
  3. Only on Deferred, escalate horizontally — trigger Cluster Autoscaler / CAPH to provision a bigger or additional node, exactly the "provision a new pod and cut over" path from before, now scoped to the actual capacity-shortfall case instead of every resize.

Reimplementing step 1 by hand-rolling delete-and-recreate — the workaround this feature exists to retire — means paying pod-churn latency and connection drops for every resize, including the overwhelming majority that a node already has headroom for. A Cluster-API-managed fleet exists specifically because the platform owns the machines and the scheduling story end to end; treating in-place resize as the default path and horizontal provisioning as the exception is what actually using that ownership looks like.

One adjacent piece of context worth flagging without overselling it: v1.35 also shipped native Gang Scheduling as an alpha feature (via a new Workload API and GangScheduling feature gate), letting a group of interrelated pods — an AI training job's workers, say — get scheduled all-at-once or not at all instead of partially landing and stalling. It's a different primitive solving a different problem (multi-pod scheduling atomicity, not single-pod resource mutation), and it's alpha rather than GA, so it's not a production dependency yet — but for a platform whose node pool might eventually serve tenant AI/agent workloads, it's the same story one release behind: a primitive worth building toward rather than reimplementing badly in the meantime.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, managed by a Cluster-API control plane you can actually read. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex