Memory QoS graduates to beta and ships enabled by default in Kubernetes 1.37
Kubernetes Memory QoS, which guides the Linux kernel on container memory handling through cgroup v2, graduates to beta and turns on by default in version 1.37. The more important change lies elsewhere: the implicit throttling factor disappears, making the upgrade a no-surprise event for existing clusters.
Kubernetes 1.22. Memory QoS appears in alpha. Kubernetes 1.36. The feature gains tiered memory reservation. Kubernetes 1.37. It graduates to beta and turns on by default on every node. Why it matters: memory handling is the leading cause of OOM kills and node instability, and Kubernetes just changed how it drives that — without changing your clusters’ behavior at upgrade time.
Memory overcommit has always been the hard part of Kubernetes scheduling. Requests and limits govern placement, but they say nothing about what the kernel should do when a node actually runs short — that decision belongs to the OOM killer, which is blunt and often kills the wrong process at the wrong moment. Memory QoS is Kubernetes’ attempt to hand the kernel a finer set of instructions before the situation escalates to a kill.
What Memory QoS actually does
Memory QoS builds on the cgroup v2 memory controller, available only on Linux nodes that use it. The idea is to give the kernel better guidance on how to treat each container’s memory, beyond the raw limits declared on Pod objects. In practice, it writes three values into cgroups:
- memory.high, which triggers throttling — the kernel slows the container to nudge it into releasing memory before it hits the hard limit;
- memory.min, a hard floor: this amount of memory is never reclaimed, even under pressure;
- memory.low, a soft floor: the memory is reclaimable as long as the rest of the system does not need more.
The goal is to replace the all-or-nothing OOM kill with a gradient: slow first, protect critical workloads, and kill only as a last resort.
One adoption caveat: Memory QoS only works under cgroup v2. Clusters still on cgroup v1 — a shrinking minority, but still present in some legacy fleets — do not benefit from it. Migrating to cgroup v2 is a prerequisite, and it is not trivial on older nodes: it is a platform project to plan, not a hot switch.
Three QoS classes, three treatments
To understand what Memory QoS changes, start from the three QoS classes Kubernetes defines:
- Guaranteed: every container declares equal memory request and limit. This is the class for critical workloads, the one you protect first.
- Burstable: request below limit. The pod may exceed its request while the node has headroom.
- BestEffort: no request or limit. First candidate for the OOM kill under pressure.
Memory QoS plays on these classes: memory.high throttling applies only to Burstable and BestEffort — Guaranteed pods are assumed to have already paid for their reservation. Tiered reservation, in turn, protects Guaranteed with memory.min and Burstable with memory.low. The result is a coherent hierarchy: guaranteed workloads are protected, burstable ones are slowed before being killed, and best-effort ones are killed first.
The change that matters: memoryThrottlingFactor becomes null
The beta promotion hides a design decision more important than it looks. In alpha, the memoryThrottlingFactor field defaulted to 0.9: enabling the feature gate was enough for the kubelet to write memory.high on containers. In 1.37, that default becomes null, meaning no memory.high is written until you configure it explicitly.
The reasoning is pragmatic. With the feature gate now on by default, keeping an automatic memory.high could have throttled workloads that previously ran without throttling. Making the value null guarantees that upgrading to 1.37 does not change runtime behavior: it is a no-surprise upgrade, which cluster operators will appreciate.
If your kubelet configuration file already carries an explicit memoryThrottlingFactor, that value is preserved and throttling keeps working. If the file does not include it, the kubelet adopts the new null default and stops writing memory.high. To keep throttling in that case, add it explicitly:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
memoryThrottlingFactor: 0.9 Enabling throttling and tiered reservation
Memory QoS is turned on through two independent kubelet configuration fields:
memoryThrottlingFactor(a value between 0 and 1): enablesmemory.highthrottling on Burstable and BestEffort containers. The kubelet computesmemory.highfrom this factor for each QoS class.memoryReservationPolicy(valueTieredReservation): enables tiered protection viamemory.minandmemory.low. The default isNone, so nothing is written.
The two can be combined or used separately. To enable both:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
memoryThrottlingFactor: 0.9
memoryReservationPolicy: TieredReservation To enable reservation without throttling, drop the first field. Finally, to disable Memory QoS entirely after an upgrade, set the feature gate to false and remove the configuration fields — otherwise the kubelet rejects the configuration.
What it changes for observability
memory.high throttling has a visible consequence in metrics: a throttled container burns more CPU per unit of work, and its latency rises before an OOM kill ever happens. That is an early signal, more useful than a brutal kill, but you have to read it. Teams that enable memoryThrottlingFactor should correlate application latency with cgroup metrics (memory.high, memory.peak, memory.events) rather than relying on the OOM kill counter alone. Otherwise, throttling shows up as an unexplained degradation.
The known limitation: a node-wide policy
memoryReservationPolicy applies to every pod on the node, with no exceptions. Under TieredReservation, every Guaranteed pod gets memory.min and every Burstable pod gets memory.low. There is no way to opt individual pods in or out: a node that mixes workloads needing hard reservation with workloads that should stay reclaimable must pick a single policy for all of them.
Hard reservation also covers everything charged to the container’s cgroup, including page cache. A pod that reads large files can therefore hold memory the kernel would otherwise reclaim to serve its neighbors. SIG Node tracks both limitations in issue kubernetes/kubernetes#140246 — the place to describe your workload if it affects you.
Take a concrete example. A node hosts both a Guaranteed database and Burstable batch jobs. Under TieredReservation, the database is protected by memory.min — that is the point. But if a batch job reads large files, its memory.low also covers its page cache, which therefore stays resident as long as the system does not ask for more. On a node where batch jobs succeed each other, that cache can shrink the reclaimable memory and accelerate pressure on BestEffort. The setting is correct, but its side effects only show over time.
To confirm whether Memory QoS is doing anything on a given node, inspect the cgroup files directly rather than trusting the feature gate alone. On a cgroup v2 node, a non-default memory.high under a container’s cgroup means throttling is active; a non-zero memory.min or memory.low means tiered reservation is in play. The kubelet only writes these values when you opt in, so an empty result is expected — and healthy — on a fresh 1.37 cluster with default settings.
Verdict
Memory QoS in beta is a clear improvement for anyone running Linux clusters under cgroup v2: throttling and tiered reservation give the kernel signals that pod limits alone cannot provide.
If your nodes suffer regular OOM kills, enable memoryThrottlingFactor around 0.9 on a pilot node first and measure the latency impact before rolling it out. If you run a mix of critical and reclaimable workloads on the same nodes, test TieredReservation while keeping its node-level granularity in mind: it protects Guaranteed pods well, but it also protects page cache, which can surprise read-heavy pods. If you are simply upgrading, you have nothing to do: the null default guarantees nothing changes as you move to 1.37.