Your node reports 62% CPU utilization. Everything looks fine — and yet one tenant's p99 latency just tripled while its rack-mate burns through a batch job. CPU percentage told you nothing was wrong because nothing about usage was wrong. Something about waiting was.
That distinction is exactly what graduated to stable in Kubernetes v1.36 "Haru": the kubelet now exports Linux Pressure Stall Information (PSI) — CPU, memory, and I/O stall time at node, pod, and container level — with the KubeletPSI feature gate locked on. It is the first built-in Kubernetes signal that measures contention instead of consumption. Here is what shipped, the four-item checklist your nodes must satisfy before you see a single number, a worked noisy-neighbor scenario showing what fires first, and one honest boundary: what GA does not do, because no kubelet flag turns on PSI-based eviction or scheduling in 1.36.
What shipped, in one paragraph: PSI has lived in the Linux kernel since 2018. Kubernetes picked it up as an alpha feature in v1.33, promoted it to beta in v1.34, and graduated the kubelet's PSI export to stable in v1.36 (released April 22, 2026). As of 1.36 you opt into nothing: the gate is locked to true and cannot be disabled. What GA is not: the scheduler and node-pressure eviction do not read PSI. There is no flag to "turn PSI-based eviction on." What you get is the signal itself — exported, per-cgroup, cheap — and the job of acting on it stays with your alerts, your autoscalers, and your controllers.
Your nodes qualify only if all four of these hold
Before the metrics, the gate. PSI is a kernel feature, and the kubelet can only report what the OS actually tracks. Every node that should emit PSI must satisfy all four:
- Linux kernel 4.20 or later. PSI entered the kernel in 4.20; anything older has no
/proc/pressureto read. - cgroup v2. Per-cgroup pressure files (
cpu.pressure,memory.pressure,io.pressure) exist only on the unified hierarchy. A node still running cgroup v1 reports nothing. - Kernel compiled with
CONFIG_PSI=y. Most modern distributions enable it by default, but some compile it in and leave it off — in which case enable it at boot with thepsi=1kernel command-line parameter, and never boot withpsi=0. - Linux nodes only. PSI does not exist on Windows. A mixed cluster is fine: Windows kubelets simply omit the metrics.
One GA hardening item matters here. Before 1.36, a kubelet with the feature enabled on a kernel without PSI support emitted misleading zero-valued metrics — zeros that read as "no contention" instead of "no data" and could silence exactly the alerts built to catch it. In 1.36 the kubelet detects OS-level PSI support from the cgroup configuration before reporting, so pressure metrics appear only where they are real. If you adopted PSI during beta, re-check any dashboard panel that ever showed a suspicious flat zero: it may have been absence disguised as health.
Verify a node in seconds: cat /proc/pressure/cpu should print some and full lines with avg10/avg60/avg300 values, and stat -fc %T /sys/fs/cgroup should report cgroup2fs. If either fails, fix the node before writing a single alert rule.
Why utilization lies, in one example
Utilization answers "how much was used." PSI answers "how much work was blocked waiting." Those sound adjacent until they diverge — and on a multi-tenant node, they diverge exactly when it hurts.
The Kubernetes documentation's own example makes the reading concrete. A container reports cpu.some avg10=0.74, avg300=0.21. That means: over the last 10 seconds, at least one task in that container spent 0.74% of wall-clock time stalled waiting for CPU across that 10-second window. Because avg10 (0.74) towers over avg300 (0.21), this is a recent surge, not a chronic bottleneck; if you watch avg300 climb toward avg10, the surge is becoming the new normal. Meanwhile cpu.full sits at 0.00: some tasks waited, but the container as a whole kept making progress. A non-zero full would mean every non-idle task stalled at once — a qualitatively worse event, and the reason some and full are separate signals rather than one number.
The vocabulary generalizes across all three resources:
| Resource | some pressure means | full pressure means | Tenant symptom |
|---|---|---|---|
| CPU | At least one task waited for a runqueue slot | All tasks stalled simultaneously (rare, severe) | p99 spikes while CPU% looks moderate |
| Memory | At least one task stalled on reclaim/swap | Everything stalled on memory at once | Mounting tail latency ahead of OOM |
| I/O | At least one task waited on storage | All tasks blocked on I/O | Build/deploy steps hang, disks "fine" |
A utilization dashboard collapses all of this into percentages that can read healthy until the node falls over. PSI preserves the shape of the contention: which resource, how recently, how widely shared.
What GA actually exports, and what it costs
Two surfaces, three granularities. The kubelet's Summary API carries .cpu.psi, .memory.psi, and .io.psi blocks at node, pod, and container level — per-tenant attribution without a sidecar:
kubectl get --raw "/api/v1/nodes/NODE_NAME/proxy/stats/summary"For Prometheus pipelines, the kubelet's /metrics/cadvisor endpoint exposes the same data as counters:
container_pressure_cpu_waiting_seconds_totalcontainer_pressure_memory_waiting_seconds_totalcontainer_pressure_io_waiting_seconds_total
The natural objection is overhead: per-cgroup stall accounting on every node sounds like a tax on the fleet. The SIG Node performance work published with the GA announcement measured it directly at 80-pod density on 4-core machines, isolating the two layers. Kernel-side bookkeeping cost 0.037 to 0.125 cores — roughly 1% to 3% of node capacity — with a single controlled spike to 0.225 cores under heavy I/O and CPU load. The kubelet's collection sweeps were lighter still: its CPU usage stayed interchangeable with the feature-off baseline, never exceeding 0.25 cores for longer than a second, blending into standard housekeeping cycles. At fleet scale that is noise — far below the cost of any per-workload agent you would run to get the same per-container attribution.
The noisy-neighbor scenario, worked end to end
Put it together on a node shared by two tenants. Tenant A launches a batch transcode that saturates the runqueue in bursts. Tenant B serves web traffic with a strict p99 budget. Node CPU utilization settles around 60% — no threshold crossed, no page fired. But Tenant B's p99 triples, because average CPU headroom says nothing about whether B's tasks get a core when they wake up.
Here is what each signal shows, in order:
- Tenant B's
cpu.some avg10lifts first — from near zero to single digits — while node CPU% barely moves. This is the earliest warning the kubelet itself can give you: tasks are waiting, not consuming. - The avg10/avg300 spread confirms it is real. A lone spike that decays is a burst;
avg60andavg300climbing behindavg10is sustained contention — the signature of a neighbor, not a blip. - Memory or I/O PSI implicates the resource. If
memory.someclimbs on Tenant B's cgroup while the node has free pages, the stall is reclaim pressure from A's allocations churning the page cache — invisible to every utilization chart on the dashboard. - Node-level PSI corroborates before you act. Per-container PSI says who hurts; node-level PSI says the machine agrees. When both point at CPU, the remediation (cap A's CPU shares, move A to a batch pool, deschedule B) rests on two independent readings instead of one tenant's complaint.
The contrast table is the whole argument in miniature:
| Moment | Node CPU% | Tenant B cpu.some avg10 | Operator conclusion |
|---|---|---|---|
| A starts batch job | 45% → 60% | 0.1 → 3.5 | Contention beginning; utilization calm |
| B's p99 triples | 62%, flat | 8+ and rising, avg60 following | Noisy neighbor confirmed; act now |
| A finishes | 60% → 30% | Decays to ~0 | Health restored; utilization never paged |
No CPU-percentage alert fires at any point in that incident. The PSI alert fires at row one.
A starter runbook: four alerts and two commands
Translate the scenario into monitoring with one rule per failure shape. Treat every threshold below as a starting point to tune against your own fleet's baseline — the signal semantics are stable, the numbers that constitute "abnormal" depend on your workloads:
- CPU contention onset:
rate(container_pressure_cpu_waiting_seconds_total[2m])rising on a container whose CPU usage is flat. Alert when short-window stall rate exceeds its own 1-hour baseline by an order of magnitude — that is the avg10-vs-avg300 spread expressed in PromQL. - Sustained CPU starvation: elevated
cpu.somepersisting across the 5-minute window. Short spikes are bursts; a 5-minute plateau is a neighbor. - Memory pressure ahead of OOM: any sustained non-zero
memory.full. Full-line memory stall means no task in the cgroup could make progress — treat it as a paging incident already in progress, more urgent than anymemory.somereading. - I/O saturation on build/storage nodes: rising
io.someon nodes backing image builds, artifact storage, or database volumes, correlated withio.fullbefore declaring the device — not the tenant — the bottleneck.
Two commands round out the kit. The Summary API one-liner from the official docs pulls per-container PSI for a named container without any monitoring stack at all — useful from a jump host during an incident. And the qualification checks from the first section (/proc/pressure/cpu, cgroup filesystem type) belong in your node-image CI: a node that silently stops emitting PSI after an OS upgrade should fail provisioning, not silently disarm every alert above it.
The honest boundary: the scheduler is not listening yet
Say it plainly so nobody misconfigures around it: in 1.36, nothing inside Kubernetes itself acts on PSI. The default scheduler still scores nodes on requested resources, not stall time. Node-pressure eviction still keys off memory.available, nodefs, and image-filesystem thresholds — a node can be PSI-screaming and evict nothing, or evict while PSI is calm. "Switching eviction and scheduling heuristics from utilization-based to PSI-based" is roadmap language, not a 1.36 feature, and any post that implies a flag flip delivers it is selling a future as a present.
What GA does unlock is everything one layer out: PSI-fed custom deschedulers that move victims off contended nodes, cluster-autoscaler pressure signals that scale on stall instead of on 80%-CPU lag, and VPA work already underway to use PSI plus memory events for reactive in-place resize — raising a pod's limits without a restart when stall data, not a usage forecast, says it is starving. The signal had to go stable before any of that could be built on without a beta disclaimer. That is the actual shape of this release: not autonomy, but the trustworthy substrate autonomy will need.
If you run your own fleet, that substrate lands directly on your to-do list: qualify the nodes, scrape the endpoint, write the four alerts, and your next noisy-neighbor incident starts with a page at row one of the table instead of a tenant ticket at row three.
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.



