FR
live

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.

An amber valve on a dark pipe in the middle of a server rack, the gauge partially closed.

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 BestEffortGuaranteed 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:

yaml
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): enables memory.high throttling on Burstable and BestEffort containers. The kubelet computes memory.high from this factor for each QoS class.
  • memoryReservationPolicy (value TieredReservation): enables tiered protection via memory.min and memory.low. The default is None, so nothing is written.

The two can be combined or used separately. To enable both:

yaml
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.

References

The cyber brief, every Tuesday

The flaws that matter and the patches to apply, in a ten-minute read.

No spam. One-click unsubscribe.
read next

On the same topic

Ingress-nginx is end of life: migrate to Gateway API before November 2026

On November 12, 2025, Kubernetes announced the retirement of ingress-nginx; upstream maintenance stopped in March 2026 and the last patch bridge, Microsoft’s for AKS, expires in November 2026. Every annotation in your fleet now has to become a Gateway API route — or remain an unpatched vulnerability.

Self-hosted GitHub Actions runners stop receiving jobs on September 25

GitHub has tightened minimum-version enforcement for self-hosted runners: brownouts began September 14, and from September 25, 2026 any runner not updated within 30 days stops receiving jobs. The failure is silent — jobs sit in “Queued” with no error: audit your runners before the next window.

← Back to the feed

Type at least two characters.

navigate open esc dismiss