Skip to main content

Kubernetes 1.36's Memory QoS Goes Tiered: What TieredReservation Actually Guarantees on a Multi-Tenant Node

8 min readDora NodaDora Noda
Share
On this page

Pack three tenants onto one node — a paying customer on a Guaranteed plan, a Burstable free-tier app, and a BestEffort background job — and for the entire history of Kubernetes, resources.requests.memory has been a scheduling hint, not a promise. The kernel's memory reclaimer doesn't know what a Pod "requested." It reclaims whatever's least recently used, and a memory-hungry neighbor can push a paying tenant's pages out under pressure even though the scheduler swore that memory was theirs. Kubernetes v1.36, released April 22, 2026, ships the piece that closes that gap: a kubelet config field called memoryReservationPolicy: TieredReservation that turns a Guaranteed pod's memory request into a kernel-enforced floor. Here's exactly what it sets, what it stops, and — just as important — the two concrete ways it still lets a node blow up if you flip it on without reading the fine print.


What TieredReservation Actually Writes, Per QoS Class

Memory QoS itself isn't new — it shipped alpha in v1.22 and reached beta in v1.27, mapping memory.high throttling off a memoryThrottlingFactor. What's new in v1.36 is a second, independent knob: memoryReservationPolicy, which controls whether a request also becomes a hard reservation, and if so, how hard per QoS class. The default, None, changes nothing — throttling only, no reservation, matching pre-1.36 behavior. Setting it to TieredReservation writes three different protection levels depending on which QoS class a pod landed in:

QoS classcgroup v2 knob setValueWhat the kernel actually does
Guaranteed (requests == limits)memory.minSum of container memory requestsNever reclaims this memory under any pressure. If the kernel can't honor it, it OOM-kills something else first.
Burstable (requests < limits)memory.lowSum of container memory requestsSoft protection — the reclaimer avoids these pages under normal pressure but will take them under sustained system-wide pressure to avoid a global OOM.
BestEffort (no requests set)NothingFully reclaimable, exactly as today.

This is set at four nested cgroup levels — the kubepods root, the per-QoS-class cgroup, the pod cgroup, and the container cgroup — with each parent's value equal to the sum of its children's, so the protection is enforced consistently whether the kernel looks at the whole node or a single container. The v1.27 version of this feature applied memory.min uniformly to any pod with a request, Guaranteed or Burstable alike — which meant a Burstable pod requesting most of a node's memory locked that memory away as hard-unreclaimable, and a node with generous Burstable requests could paint itself into an OOM corner with no soft-reclaim escape valve. TieredReservation in 1.36 is the fix for exactly that: only Guaranteed gets the hard floor; Burstable gets a floor the kernel can still step over in a genuine emergency.


The Node That Actually Changes: A Worked Three-Tenant Scenario

Take a single 8 GiB node running the mix a multi-tenant PaaS actually schedules: a paying tenant on a Guaranteed 2 GiB pod, a free-tier Burstable pod requesting 1 GiB with a 4 GiB limit, and a BestEffort batch job with no request at all. Total requested: 3 GiB out of 8 GiB allocatable — comfortably under capacity by the scheduler's own math.

With memoryReservationPolicy: None (pre-1.36 default behavior): none of the three pods has a kernel-enforced floor. If the BestEffort job leaks memory and drives the node into reclaim pressure, the kernel's LRU reclaimer treats all three pods' page caches as equally fair game. It can — and in practice does — evict pages out from under the Guaranteed tenant's paying workload while the BestEffort job that caused the pressure keeps growing, because "least recently used" has no concept of who paid for a reservation.

With TieredReservation enabled: the kubelet writes memory.min = 2Gi on the Guaranteed tenant's cgroup and memory.low = 1Gi on the Burstable tenant's. Now the same leak in the BestEffort job hits reclaim pressure and the kernel's cgroup v2 controller enforces the hierarchy: it reclaims from the BestEffort job first (zero protection), then the Burstable tenant's pages above its 1 GiB memory.low floor, and only reaches into the Guaranteed tenant's 2 GiB memory.min if literally nothing else is left to reclaim and the alternative is a node-wide OOM — at which point the kernel OOM-kills the BestEffort job instead of stealing the Guaranteed tenant's memory. That's the concrete guarantee: a paying tenant's Guaranteed memory survives a noisy neighbor's leak, full stop, as long as the sum of everyone's floors still fits in the node.

That last clause is the one operators skip, and it's exactly where the feature stops helping.


Where It Still Breaks: Over-Reservation and Page Cache

Failure mode one — the floors themselves don't fit. The Kubernetes scheduler only guarantees sum(pod requests) <= node allocatable at admission time; nothing stops an operator (or, more realistically, a bin-packing algorithm tuned for density) from admitting Guaranteed and Burstable pods whose combined memory.min + memory.low sums to more than the node physically has once you also count kubelet, container runtime, and OS overhead outside kubepods entirely. When that happens, the kernel doesn't gracefully degrade — the KEP's own design notes are explicit that if it can't honor the sum of memory.min reservations, it invokes the OOM killer to make room, which on an over-committed node means workloads start dying to protect memory floors the scheduler never actually reserved room for. TieredReservation makes reservations real; it does not make a node's arithmetic correct. That's still the operator's job, and it now has real teeth if you get it wrong.

Failure mode two — page cache on Guaranteed pods. memory.min protects a cgroup's memory from reclaim, and that includes page cache the workload built up for reads, not just anonymous (heap/stack) memory. A Guaranteed pod running a page-cache-heavy workload — think a database or a build cache with a large working set — that grows toward its memory.max limit can find the kernel unable to reclaim its own page cache to make room for new allocations, because that cache sits inside the protected memory.min floor. The KEP calls this out directly: it causes OOM kills at memory.max in scenarios where, pre-TieredReservation, reclaim would have quietly evicted stale cache pages instead. The mitigation isn't a Kubernetes setting — it's sizing the container's memory limit with real headroom above the working set for a page-cache-heavy Guaranteed workload, something an operator only needs to think about because the floor is now hard.


Rolling This Onto a Cluster API Fleet Without Guessing

Three things gate whether this is safe to flip on for a self-hosted, Cluster API-provisioned node pool rather than a footgun:

  • Kernel version ≥ 5.9. Kernels older than that have a livelock bug in the memory.high throttling path — a workload can spin allocating, hitting the limit, reclaiming, and allocating again without ever reaching memory.max where the kernel would OOM-kill it cleanly and let the scheduler recover. The kubelet logs a warning on unsupported kernels rather than refusing to start; check your CAPH node image's kernel before opting in, don't rely on the warning to catch it live in a production incident.
  • MemoryQoS is still alpha in 1.36 — it's a feature gate, not default-on, and memoryReservationPolicy defaults to None even with the gate enabled. Nothing changes for a fleet that doesn't touch the field. That also means: no upgrade-time surprise, and a green light to test it in one node pool before fleet-wide rollout.
  • Enable throttling first, reservation second. The KEP's own progressive-enablement guidance is worth following literally: turn on memoryThrottlingFactor (0.9 is the commonly cited starting point) and watch the two new alpha metrics — kubelet_memory_qos_node_memory_min_bytes and kubelet_memory_qos_node_memory_low_bytes — for a while before adding TieredReservation. Those metrics tell you, per node, exactly how much hard reservation you're about to commit before you commit it, which is the direct answer to failure mode one above: check the sum against actual node capacity before flipping the switch, not after a workload starts dying to protect a promise the scheduler never budgeted for.

For a platform whose entire pitch is bin-packing tenant workloads onto owned hardware rather than a hyperscaler's opaque isolation layer, this is the mechanism that makes "Guaranteed" mean something at the kernel, not just the scheduler. Bex provisions its node pools through Cluster API on owned Hetzner capacity specifically so operators control this layer directly — node image, kernel version, and kubelet config included — rather than inheriting whatever memory-isolation defaults a managed Kubernetes offering shipped with. If you're evaluating whether your own fleet is ready for TieredReservation, that's a kubelet-config change and a kernel check, not a platform migration.


The Honest Scope

TieredReservation doesn't add capacity, and it doesn't replace resource limits, monitoring, or sane bin-packing math — a node that over-commits memory.min reservations relative to physical RAM will still see the OOM killer show up, just with a different trigger than before. What it does add, concretely, is a kernel-enforced answer to one specific question a multi-tenant node has always had to answer on faith: when a neighbor's memory usage spikes, does a paying tenant's Guaranteed memory actually survive. As of Kubernetes 1.36, with the feature gate on, the kernel checked, and the node's floors sized correctly, the answer is yes — and it's an alpha feature precisely because getting "sized correctly" wrong is still entirely possible.


Sources: Kubernetes v1.36: Tiered Memory Protection with Memory QoS, KEP-2570: Support Memory QoS with cgroups v2, Kubernetes v1.36 Sneak Peek, Kubernetes v1.36 release notes.

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