Linux cgroup v2 memory.high and Memory Reclaim
memory.high is a Linux cgroup v2 memory limit that applies pressure before a workload reaches its hard limit: it makes the kernel reclaim memory and slow allocations rather than immediately killing a process.
A cgroup (control group) is a kernel-managed group of processes whose resource use can be measured and limited. In cgroup v2, the memory controller exposes files such as memory.current, memory.high, and memory.max in each cgroup directory.
The problem it solves
Without an intermediate limit, a service can keep allocating memory until the kernel reaches its hard boundary. At that point, the kernel may invoke the OOM killer—the out-of-memory mechanism that terminates a process to recover memory. That is a useful last resort, but it turns gradual memory pressure into a sudden failure.
memory.high provides an earlier control point. It lets an operator say, in effect:
Above this amount, preserve the workload if possible, but make it pay for the excess through memory reclaim and slower allocation.
This is useful for reducing the blast radius of a memory-hungry service, allowing caches to shrink, and exposing sustained memory growth as degraded performance before it becomes an OOM incident.
memory.high is a pressure threshold, not a wall
A cgroup administrator writes a byte value to the memory.high file. The special value max removes the limit.
printf '2G\n' > /sys/fs/cgroup/my-service/memory.high
cat /sys/fs/cgroup/my-service/memory.current
When the cgroup's usage goes above that value, allocations charged to the cgroup encounter reclaim. Reclaim is the kernel's process of finding memory that can be freed or made available—for example, by dropping clean file cache or writing reclaimable anonymous memory to swap when swap is configured.
If reclaim does not bring usage back down quickly enough, the allocating process is throttled: its attempt to obtain more memory is slowed while the kernel applies stronger reclaim pressure. The application is not normally killed merely because it crossed memory.high.
The limit is deliberately not absolute. A cgroup can temporarily exceed it, particularly when memory cannot be reclaimed immediately or when an allocation must proceed. memory.high is therefore a best-effort enforcement boundary. It controls the cost of exceeding the threshold rather than guaranteeing that usage never crosses it.
The practical result is a feedback mechanism:
- Usage approaches the threshold normally.
- Usage crosses
memory.high. - The kernel performs reclaim in the cgroup's allocation path.
- Continued allocation experiences increasing delay.
- If the workload releases memory, pressure and delay subside.
That delay can appear in an incident as unusually slow requests, allocation stalls, or high latency without an accompanying process death. The memory.events file can also report high-limit events, making the condition observable rather than requiring an OOM to reveal it.
How memory.max differs
memory.max is the hard memory boundary. The kernel still tries reclaim first when a cgroup reaches it. But if reclaim cannot reduce usage enough for a new charge, the memory controller invokes an OOM condition for that cgroup. A process in the cgroup can then be killed to make the charge possible; configuration such as memory.oom.group can affect whether the whole cgroup is treated as the killable unit.
The usual relationship is therefore:
normal allocations -> memory.high: reclaim and throttling -> memory.max: cgroup OOM
memory.high does not guarantee that memory.max will never be reached. A workload can allocate quickly, retain unreclaimable memory, or have a memory.high value close to its maximum. The high limit is an earlier pressure signal and control, not protection against every possible OOM.
Accounting through the hierarchy
Cgroups form a tree. A process belongs to one cgroup, but its memory usage is accounted not only in that cgroup: the charge is also reflected in every ancestor cgroup up to the root.
For example:
machine.slice memory.high = 8G
└── api.service memory.high = 2G
└── workers memory.high = 1G
Memory used by workers contributes to workers, api.service, and machine.slice. The values in memory.current at those levels describe the memory used by the cgroup's subtree, including descendant cgroups. A child cannot make its memory invisible to a parent's limit.
This means multiple limits can apply at once. If workers exceeds 1 GiB, its allocations encounter the child limit. If the entire api.service subtree exceeds 2 GiB, the service-level limit also creates pressure, including pressure caused by other children. If the machine-level subtree exceeds 8 GiB, the ancestor limit adds another constraint.
The memory being counted includes more than each process's private heap. Depending on the kernel and configuration, the memory controller accounts for anonymous memory, file-backed page cache, and kernel memory charged to the cgroup. Shared memory is accounted according to the kernel's memory-charge rules, so summing process-reported resident sizes is not necessarily the same as reading cgroup usage.
A further detail matters operationally: moving a process between cgroups does not mean that all memory it already caused to be allocated instantly moves with it. Charges belong to memory objects under the controller's accounting rules. New allocations and relevant charging events are what make a process's cgroup placement affect usage, so changing membership is not a reliable way to erase an existing charge.
Reading the limits operationally
Use memory.current to see current subtree usage, memory.high to see the reclaim-and-throttle threshold, and memory.max to see the hard boundary. If a service becomes slow as it approaches memory.high, that is often the intended protection working: the kernel is buying survival with latency. If it reaches memory.max and reclaim cannot help, the next symptom may be an OOM kill.
The key distinction is simple: memory.high makes excess memory expensive; memory.max makes unreclaimable excess fatal.