Linux can invoke an out-of-memory killer when an allocation cannot be satisfied within its relevant constraints after appropriate reclaim attempts. It is a last-resort recovery mechanism, not an application priority scheduler. Some allocations fail without a kill, and configuration can instead lead to panic behavior. Start an investigation by identifying the allocation scope and recorded event, rather than declaring that the machine must have exhausted every byte of RAM.
Scope comes before the score
A global memory shortage and a memory-control-group shortage are different situations. A cgroup can hit its own limit while the host still has available memory. Its OOM handling cannot simply solve that shortage by selecting an arbitrary process outside the constrained group. Other constraints, such as allowed memory nodes, can also matter. A dashboard of host-wide free memory is therefore insufficient evidence for or against a particular allocation failure.
Within the applicable selection rules, memory use and an adjustment influence victim choice. The kernel does not know which process is commercially valuable, idle-but-disposable, or essential to the business. A large database can be selected even when smaller workers contributed to pressure. oom_score is an observation exposed through procfs, not a promise that sorting every visible process predicts the next victim in every possible OOM domain.
Interpret adjustments without promising immunity
oom_score_adj ranges from -1000 to 1000. Positive values increase susceptibility and negative values reduce it; -1000 excludes a task from the kernel OOM selection addressed by that control. A modest negative adjustment does not guarantee survival. It is also not protection against explicit signals, a service manager, or a separate userspace OOM policy. Numeric adjustments cannot create missing capacity.
Protecting every task is not a workable capacity plan. If no eligible victim exists, the kernel cannot be assumed to escape a cgroup boundary and kill an unrelated supervisor. Group-kill behavior and panic settings require separate analysis. Adjustments can be inherited by children, while persistent service settings belong to the supervisor’s configuration. Changes require deliberate authorization and testing; the exercise below makes none.
When it breaks
A restart loop can hide an ongoing leak. If the same workload grows until it fails repeatedly, adding memory may merely lengthen the interval. Correlate allocation rate, resident and charged memory, workload size, reclaim, and swap activity. A process’s RSS alone does not describe all cgroup charges or all shared-memory relationships. Lowering a limit can contain growth but may make the application fail sooner.
Exit status 137 is compatible with SIGKILL; it is not exclusive evidence of OOM. Check the runtime’s termination reason, cgroup memory-event counters, and kernel records available to the operator. Distinguish a failed allocation from a killed process, and kernel OOM from userspace pressure-driven termination. A missing kernel log can reflect unavailable visibility rather than proof that no event occurred.
The score itself changes as memory and scope change. Processes can exit between procfs reads, and container PID visibility may hide much of the host. Never treat a successful read as an atomic system snapshot. Preserve the event time and affected cgroup when diagnosing an actual failure, then compare observations from the same scope.
Check it quickly
Save this as oom_observe.py and run python3 oom_observe.py with Python 3.9 or newer. It reads only its own process’s two procfs score files, allocates no pressure workload, and changes no limits or policy. Two deterministic assertions test a presentation sort on explicitly invented rows.
"""Read-only observations and a sorting test, not an OOM predictor."""
from pathlib import Path
import os
def snapshot(pid):
base = Path('/proc') / str(pid)
try:
return (int(pid), int((base/'oom_score').read_text()),
int((base/'oom_score_adj').read_text()))
except (OSError, ValueError):
return None
def ranked(rows):
return sorted(rows, key=lambda row: (-row[1], row[0]))
fixtures = [(42, 700, 0), (17, 100, -500), (99, 700, 200)]
assert [row[0] for row in ranked(fixtures)] == [42, 99, 17]
assert ranked([]) == []
row = snapshot(os.getpid())
if row is None:
print('SKIP: this process has no readable Linux OOM score files')
else:
pid, score, adjustment = row
assert -1000 <= adjustment <= 1000
print(f'pid={pid} oom_score={score} oom_score_adj={adjustment}')
print('Observed one process; no victim selection or memory pressure tested')
print('PASS: deterministic sorting checks; live observation is reported separately')
The fixture orders equal scores by numeric PID and puts the lower score last. It does not calculate kernel badness from the invented adjustment values. On Linux, the separate live branch prints the current process’s score and adjustment; without readable files it reports SKIP while still running the sorting assertions. A PASS line therefore does not imply live OOM handling was exercised.
Both exact examples passed the sorting assertions and read their own procfs files in this review’s Linux environment. No victim was selected or killed. Use this small observation to learn the interface, then investigate real events using scope-specific evidence. Changing the ranking or giving an important process a negative adjustment is never sufficient proof that the service will remain available under memory pressure.
Sources
- Linux memory concepts, allocation, reclaim and OOM boundaries, checked 2026-10-15.
- proc_pid_oom_score, reported process score, checked 2026-10-15.
- proc_pid_oom_score_adj, adjustment range and constrained allocation context, checked 2026-10-15.
- Linux cgroup v2, memory.max, OOM groups and protection scope, checked 2026-10-15.