Confirm the host runs the cgroup v2 unified hierarchy. Every
command in this lab assumes it:
Read-only / Safe— cgroup2fs means unified hierarchy (the default on Ubuntu 22.04+, Debian 12, RHEL 9). If this prints tmpfs the host is still on cgroup v1 and the memory.max paths below do not exist.
$ stat -fc %T /sys/fs/cgroup
cgroup2fs
Task 1: Bound the memory, not the host
The memory you need to constrain is the test workload’s, not
the machine’s. A cgroup limit does that precisely.
# Confirm swap is present and note how much is in use.# You are NOT going to turn it off - see the warning below.swapon --showfree -h
Task 2: Trigger an OOM inside a scope
Let systemd own the cgroup hierarchy and place the workload in
a transient scope with a real v2 memory limit:
Service impact possible— Two workers x 200 MB = 400 MB requested against a 256 MB limit, with swap denied. The cgroup OOM killer runs.
Running scope as unit: oom-test.scope
stress-ng: info: [4711] setting to a 30 second run per stressor
stress-ng: info: [4711] dispatching hogs: 2 vm
Killed
Illustrative output
The process tries to allocate 400 MB total but the cgroup
limit is 256 MB. The OOM killer will run.
oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=oom-test.scope, mems_allowed=0,oom_memcg=/system.slice/oom-test.scope, task_memcg=/system.slice/oom-test.scope,task=stress-ng,pid=12345,uid=0Memory cgroup out of memory: Killed process 12345 (stress-ng) total-vm:524288kB anon-rss:262144kB
That is not what a host-wide OOM looks like. Learn the
difference now, because it is the first question of every OOM
incident.
There is a non-log way to attribute the same kill, and it
survives log rotation:
Read-only / Safe— oom_kill counts processes this cgroup's own OOM killer has killed. A non-zero, growing oom_kill here proves the unit is exceeding its own budget - no journal needed. 'high' and 'max' count throttling and allocation stalls, which rise before the kills start.
# Substitute your own values before running:PID=1234# Find the systemd unit (if any)ps -fp "$PID"systemctl status "$PID"# Find what spawned itps -o pid,ppid,comm
Restart the service with the new settings. Try to OOM
again - with the pressure source held inside its own scope so
the host survives the test:
Service impact possible— 2 GB requested against a 1 GB ceiling. The OOM fires inside oom-pressure.scope, so the kernel picks its victim from that scope only.
# Check that the protected service is still runningsystemctl status myapp# Confirm where the kill landedsudo journalctl -k --since '5 min ago' | grep 'oom-kill'
The protected service should survive, and oom_memcg= in the
kill record should name oom-pressure.scope.
Task 7: Clean up
Leave the host exactly as you found it:
# Stop the transient scopes if they are still runningsudo systemctl stop oom-test.scope oom-pressure.scope 2>/dev/null# Remove the hand-made cgroup, if you built one in Task 2sudo rmdir /sys/fs/cgroup/oom-test 2>/dev/null# Nothing to restore for swap - you never turned it offswapon --show
Confirm no test unit survives:
Read-only / Safe
$ systemctl list-units --all 'oom-*'
0 loaded units listed.
Illustrative output
Task 8: Document
OOM LAB=======Test environment:- transient scope: oom-test.scope- MemoryMax: 256M, MemorySwapMax: 0- host swap: left enabled, untouchedOOM triggered by:- stress-ng --vm 2 --vm-bytes 200M- Tried to allocate 400 MB- Hit 256 MB limit- OOM killed stress-ngEvidence classified:- constraint=CONSTRAINT_MEMCG -> cgroup OOM, not host OOM- oom_memcg=/system.slice/oom-test.scope- memory.events oom_kill incremented for that cgroupPrevention applied:- Protected service: MemoryMax=2G, OOMScoreAdjust=-900- Disposable service: MemoryMax=512M, OOMScoreAdjust=+500Verification:- OOM still occurs under stress- Protected service survives- Disposable service is killed first
Deliverables
· OOM records captured
· Kill classified as CONSTRAINT_MEMCG or CONSTRAINT_NONE
· Identification of the offending process
· Prevention: cgroup limit or OOMScoreAdjust
Verification status
Last reviewed
2026-08-09
Executed end to end
not yet run on hardware
The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.