A kubelet configuration can look harmless in a review because it contains only two new fields. On a Kubernetes v1.37 node, memoryThrottlingFactor and memoryReservationPolicy change how the kubelet uses the cgroup v2 memory controller. The change is not confined to one container: it changes the pressure and protection behavior applied to Pods on that node.
MemoryQoS is beta and enabled by default in Kubernetes v1.37. The feature does not, by itself, turn on memory throttling or reservation. The default kubelet configuration leaves both behaviors off. A non-null memoryThrottlingFactor requests throttling, while memoryReservationPolicy: TieredReservation requests memory protection. Both decisions need a worker-node cgroup v2 prerequisite and a rollout plan that treats the configuration as node behavior, not as an application manifest.
This guide uses a small local preflight to catch two mistakes before a kubelet configuration reaches a node: an invalid throttling factor and a MemoryQoS configuration applied where cgroup v2 is not available. The validation used Python 3.13.5 on Linux with synthetic JSON-form kubelet configurations. It accepted a configuration with a factor of 0.9 and TieredReservation, rejected a factor of zero, and rejected the same otherwise-valid configuration against a synthetic non-cgroup-v2 root. No cluster, workload, or production node was changed.
Know which control changes which cgroup setting
memoryThrottlingFactor controls whether the kubelet writes memory.high. It must be greater than zero and no greater than one. For a Burstable workload, Kubernetes describes the threshold as the request plus the selected factor multiplied by the difference between the limit and request. A factor near one therefore defers throttling until the workload is close to its limit. It does not add memory capacity and it does not make an over-limit allocation safe.
memoryReservationPolicy controls memory protection. None is the default. TieredReservation asks the kubelet to use memory.min for Guaranteed Pods and memory.low for Burstable Pods. Those are different promises: memory.min is unreclaimable protection, whereas memory.low is preferred protection that can still be reclaimed under severe pressure. The kubelet also configures ancestor cgroups so that the per-Pod settings can take effect.
The operational consequence is easy to miss. A guaranteed Pod has equal memory request and limit. With TieredReservation, its protected minimum is therefore also its hard ceiling. Kubernetes cautions that a workload with a large page cache can be OOM-killed if that ceiling has no headroom. Do not introduce a reservation policy by copying a factor from an unrelated workload. Start from observed requests, limits, cache behavior, and node pressure in staging.
Both controls require Linux with cgroup v2. Kubernetes recommends kernel 5.9 or later because older kernels can encounter a documented livelock issue around memory.high. A generic CI runner can prove that a JSON file follows a local policy, but its /sys/fs/cgroup says nothing about the worker nodes that will run the kubelet. Run the node-facing part of this check on a staging node, a node image, or a controlled mount of that node’s cgroup filesystem.
Keep the kubelet configuration explicit
Kubelet configuration is normally YAML. JSON is valid YAML, which makes JSON fixtures convenient for an offline test without bringing a YAML parser into a small review tool. This example requests both supported behaviors:
{
"apiVersion": "kubelet.config.k8s.io/v1beta1",
"kind": "KubeletConfiguration",
"memoryThrottlingFactor": 0.9,
"memoryReservationPolicy": "TieredReservation"
}
A factor of 0.9 means the kubelet has a positive threshold to configure. It is not a universal tuning value. A low factor starts throttling earlier; a value closer to one leaves less distance between memory.high and the hard limit. Set it only after deciding what latency and reclaim behavior the workload can tolerate.
The reservation policy is independent. Operators can enable throttling without tiered reservation, or tiered reservation without throttling. Omitting both fields leaves the documented defaults in place. That is often the right first state when the node is not running cgroup v2 or when a staging exercise has not established the effect on the service.
Do not infer a feature’s availability from the Kubernetes client version alone. The relevant control plane and kubelet versions, the MemoryQoS feature gate, the node kernel, cgroup hierarchy, and kubelet configuration all matter. In v1.37 the feature gate is beta and enabled by default, but a mixed-version cluster or an explicit gate override still deserves an inventory check before configuration rollout.
Test the preflight before node configuration is applied
The following script deliberately checks only configuration shape and the local cgroup v2 marker. It reads a JSON-form kubelet configuration and looks for cgroup.controllers, the cgroup v2 controller file, below the supplied root. It does not parse a cluster inventory, modify a kubelet, or predict workload latency.
#!/usr/bin/env python3
import argparse
import json
import sys
from pathlib import Path
parser = argparse.ArgumentParser()
parser.add_argument("config", type=Path)
parser.add_argument("--cgroup-root", type=Path, default=Path("/sys/fs/cgroup"))
args = parser.parse_args()
config = json.loads(args.config.read_text(encoding="utf-8"))
factor = config.get("memoryThrottlingFactor")
policy = config.get("memoryReservationPolicy", "None")
failures = []
if factor is not None and (
not isinstance(factor, (int, float)) or isinstance(factor, bool) or not 0 < factor <= 1
):
failures.append("memoryThrottlingFactor must be a number greater than 0 and no greater than 1")
if policy not in {"None", "TieredReservation"}:
failures.append("memoryReservationPolicy must be None or TieredReservation")
if (factor is not None or policy == "TieredReservation") and not (args.cgroup_root / "cgroup.controllers").is_file():
failures.append("MemoryQoS requires a cgroup v2 root containing cgroup.controllers")
if failures:
print("\n".join(failures), file=sys.stderr)
raise SystemExit(1)
print("PASS: MemoryQoS configuration is valid")
Keep the complete version of a policy like this with its configuration-management code, not inside a blog post. In particular, production tooling should handle file errors and rejected JSON with a distinct exit status so an unreadable input cannot appear to be a policy failure. The narrow rule is useful because it separates a configuration error from the later question of whether the node and workloads behaved correctly.
Run it before the automation that writes a kubelet configuration. In a disposable directory, a positive case should return zero:
python3 validate_memory_qos.py memory-qos.json
The local validation printed PASS: MemoryQoS configuration is valid; MemoryQoS is enabled. The host cgroup root contained cgroup.controllers, so the test established the expected v2 marker without inspecting any workload or cluster data.
Test a negative fixture as well. A value of zero is not an enabled throttle factor, even though it is a number that may look plausible in a configuration review:
{
"apiVersion": "kubelet.config.k8s.io/v1beta1",
"kind": "KubeletConfiguration",
"memoryThrottlingFactor": 0
}
The tested preflight returned exit status one and printed:
memoryThrottlingFactor must be a number greater than 0 and no greater than 1
A second negative test passed the valid JSON to an empty synthetic cgroup root. It also returned one, with MemoryQoS requires a cgroup v2 root containing cgroup.controllers. That result is the boundary of this check: it proves the prerequisite can be made visible before rollout, not that a real worker node has been inspected. Do not point a CI job at its own cgroup filesystem and call that a worker-node result.
Finish with a staged node check
After the static gate passes, select an authorized staging node with representative cgroup v2, kernel, and workload characteristics. Record the pre-change kubelet configuration, node version, feature-gate state, and the relevant cgroup files using redacted change evidence. Apply one reviewed change through the existing node-management process; do not edit a production kubelet in place for this test.
Observe both a Burstable workload and, if the policy uses reservation, a Guaranteed workload. Confirm the expected memory.high, memory.low, or memory.min values in the appropriate cgroups, then watch application latency, OOM events, and node-pressure behavior. The configuration is not validated merely because the kubelet restarted. A service can remain healthy at idle load while reacting badly when cache use or memory pressure rises.
Have a rollback ready before enabling either setting. To disable MemoryQoS behavior, Kubernetes documents setting the feature gate to false, leaving memoryReservationPolicy unset or None, and restarting the kubelet. Existing per-Pod values can remain but become ineffective when ancestor protection is reset. For a normal configuration rollback, remove the requested setting through the same node-management system and verify the replacement state after the kubelet restart.
MemoryQoS turns a resource declaration into a node-level cgroup policy. An offline preflight catches obvious configuration and platform mistakes early. The staging check then answers the question the preflight cannot: whether the selected limits, requests, kernel, and workload behavior produce a safe result under pressure.