Where to start First steps track · 15 stages
Linux Labs

OOM killer: when memory runs out

12 min Ubuntu 24.04 · Linux 6.8 · systemd 255

A kernel mechanism: when memory runs out on the machine or in a cgroup with a limit, it kills a process with SIGKILL.

Cheat sheet

7 commands Printable version
  • sudo journalctl -k -o cat | grep 'Killed process' who was killed
  • systemctl show app -p Result -p NRestarts Result=oom-kill
  • systemctl show app -p MemoryMax -p MemoryPeak limit and peak
  • cat /sys/fs/cgroup/system.slice/app.service/memory.events
  • choom -p 1234 score and adjustment of a process
  • sudo choom -n -500 -p 1234 protect a process from OOM
  • free -h available — the machine's memory reserve

Overview

The OOM killer (out of memory) is the part of the kernel that frees memory when there is nowhere else to get it. First the kernel evicts the page cache and, if there is swap, rarely used program pages. When nothing is left to free, the OOM killer picks one process and ends it with SIGKILL. This signal cannot be caught: the program has no time to write anything to its log.

The machine or a cgroup

Memory runs out in two places:

Where When Kernel log line Candidates
whole machine system memory and swap are exhausted Out of memory: Killed process all processes of the machine
cgroup the group hit its own limit Memory cgroup out of memory: Killed process processes of this group

A group limit is MemoryMax= in a systemd unit or --memory for a container (see cgroups-namespaces). It protects the rest of the machine: a service with MemoryMax=64M hits its own 64 MiB while neighbours keep working. That is why a cgroup OOM happens while free -h shows free memory.

A machine-wide report differs: the oom-kill: line carries constraint=CONSTRAINT_NONE and global_oom, and the result is Out of memory: Killed process 1500 (python3) … oom_score_adj:1000. That is how a python3 that took 400 MiB on a machine without swap ended.

How the victim is chosen

The kernel gives every process a score, oom_score: the more memory it holds (RSS, swap, page tables), the higher the score. The adjustment oom_score_adj, from -1000 to 1000, shifts it: -1000 means never touch, 1000 means kill first. The process with the highest score is killed.

text
student@ubuntu:~$ choom -p $$
pid 954's current OOM score: 673
pid 954's current OOM score adjust value: 0
student@ubuntu:~$ sudo choom -n 500 -p $$
pid 954's OOM score adjust value changed from 0 to 500
student@ubuntu:~$ choom -p $$
pid 954's current OOM score: 1006
pid 954's current OOM score adjust value: 500

systemd sets a service's adjustment from the unit's OOMScoreAdjust=: systemd-journald has -250. stress-ng gives its worker processes 1000 so that OOM hits them.

What the kernel writes

An OOM report in the kernel log runs to dozens of lines: a call stack, the group's memory, a task list. grep finds the main lines:

bash
sudo journalctl -k -o cat | grep -E \
  'invoked oom|memory: usage|oom-kill:|Killed process'

The service hog with MemoryMax=64M, where tail piles its input up in memory, produced these lines:

Line What to take from it
tail invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0 who asked for memory at the moment of shortage; another process may become the victim
memory: usage 65536kB, limit 65536kB, failcnt 597 the group hit its 64 MiB limit
oom-kill:constraint=CONSTRAINT_MEMCG,…,oom_memcg=/system.slice/hog.service,…,task=tail,pid=1216,uid=0 CONSTRAINT_MEMCG — shortage in a cgroup; oom_memcg — which group: a unit or a container
Memory cgroup out of memory: Killed process 1216 (tail) total-vm:70808kB, anon-rss:64768kB, … the victim's PID and name; anon-rss — the program memory it held

Before the last line the kernel prints the group's tasks with their memory in 4 KiB pages (middle columns omitted here):

text
Tasks state (memory values in pages):
[  pid  ]   uid  tgid total_vm      rss … oom_score_adj name
[   1214]     0  1214      700      448 …             0 sh
[   1215]     0  1215     1531      448 …             0 head
[   1216]     0  1216    17702    16640 …             0 tail

The kernel log is readable by root and the groups adm and systemd-journal. Without sudo, journalctl -k prints -- No entries -- and dmesg prints Operation not permitted.

What systemd sees

systemd learns about an OOM in a service's group and stops the whole service (OOMPolicy=stop by default): its other processes get SIGTERM. The service result is oom-kill:

text
student@ubuntu:~$ sudo journalctl -u hog -o cat
Started hog.service - /usr/bin/sh -c "head -c 300M /dev/zero | tail".
hog.service: A process of this unit has been killed by the OOM killer.
hog.service: Failed with result 'oom-kill'.
student@ubuntu:~$ systemctl show hog -p Result -p NRestarts
Result=oom-kill
NRestarts=2

With Restart=on-failure the service comes back, hits the same limit, and NRestarts grows. The group's counters are in memory.events. The service vmhog is stress-ng under MemoryMax=96M with OOMPolicy=continue: it survives OOM, and stress-ng starts its worker again.

bash
cat /sys/fs/cgroup/system.slice/vmhog.service/memory.events
text
low 0
high 0
max 15070
oom 102
oom_kill 102
oom_group_kill 0

max is how many times the group hit MemoryMax, oom_kill is how many of its processes the OOM killer ended. The file exists while the service runs.

Exit code 137 and Killed

A process killed with SIGKILL exits with code 137 = 128 + 9 (see exit-codes). A shell whose child died prints Killed. A Docker container after OOM shows Exited (137) and OOMKilled: true (see docker).

Recipes

4

Find whom OOM killed and when

bash
sudo journalctl -k -o short-iso | grep 'Killed process'
sudo journalctl -k -b -1 | grep -i oom    # previous boot
sudo dmesg -T | grep -i 'killed process'

Match the time against the service's journal (journalctl -u <service>) to see what the service was doing before it died.

A memory limit for a service

The limit goes into a drop-in, a file that extends the unit (see drop-in):

bash
sudo mkdir -p /etc/systemd/system/demo.service.d
printf '[Service]\nMemoryMax=256M\n' \
  | sudo tee /etc/systemd/system/demo.service.d/memory.conf
sudo systemctl daemon-reload
sudo systemctl restart demo
systemctl show demo -p MemoryMax -p DropInPaths
text
MemoryMax=268435456
DropInPaths=/etc/systemd/system/demo.service.d/memory.conf

MemoryHigh= is a soft threshold: above it the kernel reclaims the group's memory and slows it down, without killing. stress-ng with 100 MiB under MemoryHigh=64M finished without OOM, but ran 13 seconds instead of 8. How much a service uses now and at most is MemoryCurrent and MemoryPeak in systemctl show.

Protect an important process

bash
sudo choom -n -500 -p 1234          # a running process
# in the unit: [Service] OOMScoreAdjust=-500

Only root can set a negative adjustment. It moves the process in the queue but adds no memory: if the important process ate everything itself, OOM takes its neighbours and then the process too.

Reproduce OOM safely

bash
sudo systemd-run --unit=hog -p MemoryMax=64M \
  sh -c 'head -c 300M /dev/zero | tail'

tail waits for a newline and keeps all input in memory until it hits the 64 MiB of its group. The machine does not suffer. Memory load with a timer also comes from stress-ng: --vm 1 --vm-bytes 160M --timeout 10s.

Troubleshooting

5

A service restarts in a loop

systemctl status shows activating (auto-restart) (Result: oom-kill), and NRestarts grows. The service hits MemoryMax on every start: the limit is below what it needs, or memory leaks. Compare MemoryPeak with MemoryMax, raise the limit with a drop-in or find the leak in the application.

A process vanished without a line in its log

SIGKILL lets the program write nothing. Look in the kernel log: sudo journalctl -k | grep -i 'killed process'. The service journal keeps Killed from the shell or exit code 137.

Result=exit-code, status=137

The main shell process managed to exit by itself — with code 137 after OOM killed its child — before systemd received the OOM event:

text
Killed
lab-hog.service: Main process exited, code=exited, status=137/n/a
lab-hog.service: Failed with result 'exit-code'.

It is the same OOM. Confirm it with the Memory cgroup out of memory line at that time in journalctl -k.

OOM with free memory

free -h shows the memory of the whole machine, while the OOM happened in a cgroup. The group limit is in systemctl show <service> -p MemoryMax or the memory.max file under /sys/fs/cgroup/…; for a container, in docker inspect.

OOM killed the wrong process

The process in the invoked oom-killer line only asked for memory at the moment of shortage. The victim is chosen by oom_score — usually the largest process of the group or machine. Compare the victim's anon-rss with the others in the Tasks state table.

Danger zone

1

Adjustment -1000 for a hungry process

With oom_score_adj=-1000 the process is never touched. If it is the one that leaks, OOM kills everything around it, up to sshd and the journal. Keep -1000 for processes whose memory does not grow.

Terms

1
OOM score (oom_score)
The number the OOM killer uses to pick a victim: it grows with the process's memory, and the oom_score_adj adjustment from -1000 to 1000 shifts it. Shown in /proc/<pid>/oom_score and choom -p. Also appears in: /proc: processes and the kernel as files, stress-ng

See also

  • memory-swap — free, available, page cache and swap.
  • cgroups-namespaces — groups and their limits.
  • systemd — units and drop-ins.
  • journalctl — the kernel log -k and a service log -u.
  • exit-codes — code 137 and other 128+N codes.
  • docker — a container's memory limit and OOMKilled.
  • kill — the signals SIGTERM and SIGKILL.
Loading…