Skip to main content

Your Pod Can Now Evict Its Neighbors to Grow: Scheduler Preemption for In-Place Resize Goes Alpha in Kubernetes v1.37

9 min readDora NodaDora Noda
Share
On this page

Your highest-priority pod needs 2 more CPUs right now or it OOMs, the node it sits on is full, and Kubernetes' answer until last month was: wait. Not "no" — the resize request was perfectly valid — just an indefinite later, parked in a state literally called Deferred, until some other pod happened to leave. On September 10, 2026, Kubernetes v1.37 closed that gap with scheduler preemption for in-place pod resize (alpha): the scheduler can now evict lower-priority pods on the same node to make room for a growing pod, instead of leaving the resize parked forever.

Here is the whole feature in one lifecycle. A high-priority pod requests more CPU than its node has free; the kubelet marks it ResizeDeferred; the scheduler preempts a lower-priority neighbor; the kubelet actuates the resize without restarting anything. Four events, in order: ResizeDeferredPreemptedResizeStartedResizeCompleted. In the upstream demo — an 8-CPU node holding a 3-CPU low-priority pod and a 4-CPU high-priority pod — a 4-to-6 CPU resize that could never fit completes live, and allocatedResources.cpu reads 6. That sequence is the deliverable of this post: what each step means, how to reproduce it in ten minutes on kind, and what changes for a self-hosted PaaS bin-packing tenant workloads onto a fixed set of owned nodes.

How we got here: KEP-1287 in one minute

In-place pod resize is the six-year project (KEP-1287) that made pod.spec.containers[].resources mutable for CPU and memory, so the kubelet can rewrite cgroup limits at runtime instead of restarting the container. It went alpha in v1.27 (April 2023), beta in v1.33, and GA in v1.35 (December 2025), where the gate locked on. Memory can be configured to restart the container (not the pod) via resizePolicy, and every resize reports its state through pod conditions and events.

The two stuck states matter for everything below, so here is the distinction the resize docs draw:

StateMeaningWho can fix it
InfeasibleImpossible on this node: exceeds physical capacity, namespace limits, or admission quotasNobody on this node — the request itself must change
DeferredValid but temporarily unactuable: the node is full right nowAnyone who frees capacity — churn, eviction, or now the scheduler

Before v1.37, Deferred had three exits, and Natasha Sarkar's announcement post (Google, September 10) lists them bluntly: an operator manually evicts something, the cluster autoscaler moves the pod to a bigger node (a restart that voids the entire "no restart" promise of in-place scaling), or a custom autoscaler resizes the node itself. The scheduler — the one component built to arbitrate exactly this kind of contention — never saw the request, because a running pod with spec.nodeName set bypasses the scheduling queue entirely. That blind spot is what the alpha removes.

How the alpha works

The feature hides behind the InPlacePodVerticalScalingSchedulerPreemption gate, alpha and disabled by default in v1.37. All five mechanics below come from the upstream design; each is one concrete behavior you can observe.

The scheduler watches Deferred pods. With the gate on, kube-scheduler tracks running pods carrying the Deferred resize condition and keeps them in active scheduling evaluation specifically to trigger preemption — even though they are already placed. Tracking continues until the kubelet reports the resize actuated.

Preemption is strictly single-node. Placement preemption scans the cluster for the best fit; resize preemption only looks at the pod's current node. The scheduler picks lower-priority victims on that same host and gracefully evicts them. If the resize still cannot fit after every eligible victim is gone, it stays Deferred — the feature never migrates the pod, which would defeat the purpose.

Resize resources count as already consumed. To avoid double-booking, the scheduler treats the requested growth as spent capacity from the moment it starts working the resize. That reservation is what lets the kubelet actuate the moment preemption frees the room, without a race against another placement landing in the freed space.

The kubelet delegates; PDBs and graceful termination hold. Under the gate, the kubelet's critical-pod admission handler does not do local evictions for resizes. It marks Deferred and hands the decision to the scheduler, so one centralized orchestrator applies global priorities, honors PodDisruptionBudgets, and follows graceful termination. If a competing higher-priority resize lands on the same node mid-cycle, the kubelet prefers it and the scheduler opens a fresh preemption round.

Nodes can opt out. Admins and controllers get a per-node escape hatch, spec.podPreemptionPolicy.disableResizePreemption, listing controller keys that suppress scheduler preemption for resizes on that node — for setups where a controller would rather shrink other pods or grow the node itself first and keep preemption as a last resort:

yaml
apiVersion: v1
kind: Node
metadata:
  name: batch-workload-node
spec:
  podPreemptionPolicy:
    disableResizePreemption:
      - "cluster-autoscaler.kubernetes.io/disable-preemption"
      - "operator.example.com/policy-override"

Requirements to run it: v1.37 or later on the control plane and every worker node, with the gate enabled on kube-apiserver, kube-scheduler, and the kubelet.

Try it in ten minutes on kind

The upstream post ships a single-node reproduction, condensed here. It needs only kind and kubectl.

1. Create a v1.37 cluster with the gate on. Save this as kind-config.yaml:

yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  InPlacePodVerticalScalingSchedulerPreemption: true
shell
kind create cluster --config kind-config.yaml --image kindest/node:v1.37.0
kubectl get nodes -o custom-columns=NAME:.metadata.name,ALLOCATABLE_CPU:.status.allocatable.cpu

A stock local node reports 8 allocatable CPUs.

2. Deploy a 3-CPU low-priority pod and a 4-CPU high-priority pod. Together they consume 7 of 8 CPUs, leaving 1 free. Save all four documents below into one preemption-demo.yaml, separated by YAML document separators:

Two PriorityClasses first:

yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: "High priority workload"
yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: low-priority
value: 1000
globalDefault: false
description: "Low priority workload"

Then the two pods:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: low-priority-pod
spec:
  priorityClassName: low-priority
  containers:
  - name: worker
    image: nginx
    resources:
      requests:
        cpu: "3"
        memory: "500Mi"
      limits:
        cpu: "3"
        memory: "500Mi"
yaml
apiVersion: v1
kind: Pod
metadata:
  name: high-priority-pod
spec:
  priorityClassName: high-priority
  containers:
  - name: app
    image: nginx
    resources:
      requests:
        cpu: "4"
        memory: "1Gi"
      limits:
        cpu: "4"
        memory: "1Gi"
shell
kubectl apply -f preemption-demo.yaml

3. Request the impossible resize. Patch the high-priority pod from 4 to 6 CPUs — a +2 delta against 1 free CPU:

shell
kubectl patch pod high-priority-pod --subresource resize --patch \
  '{"spec":{"containers":[{"name":"app", "resources":{"requests":{"cpu":"6"}, "limits":{"cpu":"6"}}}]}}'

4. Watch the lifecycle. The low-priority pod takes the hit:

shell
kubectl get events --field-selector involvedObject.name=low-priority-pod

Expect a scheduler-emitted Preempted followed by Killing. Then the grower walks the full sequence:

shell
kubectl get events --field-selector involvedObject.name=high-priority-pod

ResizeDeferred (warning, OutOfcpu: requested: 6000, used: 3950, capacity: 8000) → ResizeStartedResizeCompleted, after which kubectl get pod high-priority-pod -o jsonpath='{.status.containerStatuses[0].allocatedResources.cpu}' prints 6. No restart, no reschedule — the pod grew in place by spending its neighbor.

What it means for a PaaS on fixed nodes

For a platform packing bursty tenant pods onto a capped set of owned machines — the Hetzner-fleet shape this blog keeps coming back to — the pre-v1.37 dilemma was the one Sarkar names: either run low-utilization clusters with idle buffer on every node, or bin-pack tightly and accept that a tenant's vertical scale-up could wedge in Deferred until natural churn or a manual drain freed room. Resize preemption dissolves the dilemma in one direction: filler workloads stop being a hazard to growers. Pack the node; the scheduler sorts out who yields.

But the honest version of that sentence has three footnotes, and they are all about policy, not mechanism.

Your PriorityClasses just became your resize policy. Preemption only flows downhill — a pod can only displace strictly lower-priority pods — so the class ladder you may have set up casually for placement now decides whose 3 a.m. memory spike succeeds and whose batch job gets evicted to pay for it. Design it as tiers with real gaps, for example tenant web traffic at 1,000,000, async workers at 100,000, and batch/sandbox filler at 1,000, and document that the ladder is load-bearing for vertical scaling, not just scheduling order. Every resize preemption also inherits the standard guardrails: PDBs are respected and termination is graceful, which means filler without a PDB is filler that can vanish mid-resize — fine for idempotent batch, dangerous for anything holding uncheckpointed state.

The victim still needs somewhere to go. Evicting the neighbor frees room for the grower, but on a fully packed fixed fleet the evicted pod re-enters scheduling with no free space anywhere and sits Pending until room appears. That is the correct trade — the critical workload grew — but it means resize preemption converts "grower waits" into "victim waits," and under correlated tenant growth (every tenant spiking together on the same morning traffic, or a shared downstream slowing down and inflating memory everywhere at once) you can get preemption churn: resizes cascading evictions faster than the victims can reland. Mitigate with PDBs on anything that must keep quorum, generous priority gaps so only true filler is displaceable, and alerts on Preempted event rate per node — a preemption storm looks exactly like success from the grower's side.

It composes with the rest of the 1.37 scheduling story. Two siblings matter. HPA scale-to-zero graduated to beta enabled by default in 1.37 (KEP-2021, for workloads scaling on object or external metrics), so idle queue workers can now vacate nodes entirely and reduce the contention resizes compete against. And workload-aware preemption (KEP-5710) also went beta, teaching the scheduler to weigh whole PodGroups rather than individual pods when evicting — resize preemption today is still per-pod and single-node, so group-aware resize arbitration is the obvious next watch item, not today's behavior.

The adoption checklist, then: v1.37 on control plane and all nodes, the gate on apiserver/scheduler/kubelets together, a deliberate PriorityClass ladder, PDBs on filler that holds state, and — this is alpha, disabled by default, with the API still free to change — a test fleet first. The disableResizePreemption node policy exists precisely so autoscaler-driven node pools can keep their current behavior while you evaluate.

The no-restart promise, completed

In-place resize made growing without restarting stable in v1.35; v1.37's alpha addresses the scheduling reality a full node made of that mechanism — a valid request with nowhere to go. The design choices are the conservative ones: same node only, PDBs honored, kubelet defers to the scheduler instead of evicting locally. For self-hosted platforms that buy their headroom in whole servers rather than per-second vCPU, that conservatism is the point: tighter bin-packing with a documented, priority-driven answer to "who yields when everyone grows at once."

Watch the beta for group-aware behavior and wider controller support; until then, the ten-minute kind demo above is the cheapest way to feel the new lifecycle. And if the broader theme here — declarative machine lifecycle on hardware you own, where the scheduler rather than a runbook arbitrates contention — is the platform you want to run, that is what we are building.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. 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