Understanding the OOM Killer and Avoiding Memory Problems
AI generated
$
/etc
Linux · Kernel · Memory Management · Troubleshooting
Understanding the OOM Killer and Avoiding Memory Problems
oom_score, dmesg analysis, and targeted cgroup limits

When the Linux kernel comes under memory pressure, the OOM killer decides within a split second which process must die, often the database or PHP-FPM pool instead of the actual memory hog. This article explains the oom_score calculation, shows how to trace OOM kills in dmesg and journalctl, and how to protect critical services through oom_score_adj and cgroup limits.

17 min read oom_score · oom_score_adj · cgroup v2 dmesg · journalctl · systemd-oomd

1. What the OOM killer actually does

The OOM killer (Out-Of-Memory killer) is a kernel mechanism that forcibly terminates a process when the kernel can no longer satisfy a memory request, even after trying to free memory first. It is not a bug or a malfunction, but a deliberate design decision: a system that freezes completely because it cannot allocate any more memory is worse than a system that sacrifices a single process in order to keep running. Without this mechanism, a kernel panic would be imminent the moment the last free memory block is consumed.

The distinction from swapping matters here: as long as swap space is still available and the kernel can page memory out to disk, the OOM killer does not step in, even though the system becomes noticeably slower in the process. Only once reclaim attempts, swap-out, and memory compaction together fail to satisfy the requested page does the OOM killer get invoked directly from the page allocator in kernel context. The result is almost always a SIGKILL, the process gets no chance at a clean shutdown.

2. How the kernel detects memory pressure

Linux allows memory overcommit by default: processes can reserve more virtual memory than is physically present, because many allocations are never fully written to. This is controlled through /proc/sys/vm/overcommit_memory with three modes: 0 uses a heuristic estimate, 1 permits essentially unlimited overcommit, and 2 enforces strict accounting based on vm.overcommit_ratio plus swap size. Only once processes actually write to the reserved memory does it become physically used, and that is exactly where memory pressure can emerge.

Before the OOM killer intervenes, the kernel tries to free memory through kswapd running in the background and through direct reclaim in the allocation path itself: dropping unused page cache pages, writing anonymous pages out to swap, defragmenting memory. Under cgroup v2 there is an additional, self-contained reclaim and OOM mechanism per cgroup: once a service hits its memory.max limit, that triggers an OOM kill inside that cgroup, regardless of how much free memory is still available on the rest of the system. This separation is the key to controlled memory behavior in containerized environments.

3. Picking the victim: oom_score and the badness calculation

When the OOM killer becomes active, the kernel calculates a so-called badness score for every eligible process, visible under /proc/PID/oom_score. The basis of this calculation is roughly the share of actually used memory (RSS plus swap usage) relative to the total memory available, expressed in per-mille. A process consuming forty percent of system memory tends to receive a noticeably higher score than a small background service, even if both have been running for the same amount of time. The process with the highest score gets killed, not necessarily the process that triggered the current allocation.

This score can be influenced through /proc/PID/oom_score_adj, a value between -1000 and 1000. The value -1000 excludes a process from OOM selection entirely, the kernel itself uses this for init/PID 1 and typically for systemd, so the system remains functional at all after an OOM kill. Positive values increase the likelihood of being killed, negative values decrease it. The older oom_adj interface (range -17 to 15) is marked deprecated, should no longer be used in new scripts, and only exists for backward compatibility.

4. Finding OOM kills in dmesg and journalctl

The typical symptom of an OOM kill: a service dies seemingly without cause, with no error message in its own log, no core dump, often with exit code 137 (128 plus signal 9 for SIGKILL). The first place to look is always the kernel ring buffer: dmesg -T | grep -i "killed process" shows the kill line with timestamp, process name, and PID. On a system running systemd-journald, journalctl -k --since "-1h" delivers the same kernel messages persistently across reboots, which dmesg alone cannot do since its buffer is lost on reboot.

A complete OOM kill message contains far more than just the kill line: it starts with invoked oom-killer, then lists a table of all candidate processes along with their respective oom_score_adj and memory usage, and ends with the actual Killed process line including total-vm, anon-rss, and file-rss. This table is essential for diagnosis, because it shows why exactly this process, and no other, was selected. For containerized services, docker inspect <container> --format '{{.State.OOMKilled}}' adds the container context on top of the kernel view.


# Search the kernel ring buffer for OOM kills
dmesg -T | grep -i "killed process"
# [Sat Jul 11 03:14:22 2026] Killed process 18422 (mysqld) total-vm:4823112kB, anon-rss:3912456kB, file-rss:2048kB

# Persistent search via journald, survives a reboot
journalctl -k --since "-24h" | grep -iE "oom|killed process"

# Print the full OOM context around one event
journalctl -k -o short-precise | grep -A 40 "invoked oom-killer" | head -n 50

# Check the exit code of a systemd service (137 = SIGKILL after OOM)
systemctl status mysql.service | grep -i "code="

# Query the OOM status of a Docker container directly
docker inspect webapp --format '{{.State.OOMKilled}} exit={{.State.ExitCode}}'

5. Tuning oom_score_adj for critical services

To prevent the database or the central cache service from dying first while a buggy batch job allocates memory unchecked, oom_score_adj can be set explicitly per service. For systemd-managed services this is done declaratively via the OOMScoreAdjust= unit directive in the service file, or via an override created with systemctl edit mysql.service. For processes that are already running, the util-linux package ships the choom command, which lets you change the value at runtime without restarting the process.

The side effect matters here: oom_score_adj does not change the total memory of the system, it only shifts which process gets sacrificed in an emergency. If the database is fully shielded (-800 or lower), the next-worst candidate takes the hit instead, in the worst case an equally critical service such as the reverse proxy or even the SSH daemon. A staged priority scheme across several services, combined with real memory limits, makes more sense than granting a single process blanket immunity.


# Read the current badness score and adjustment of a running process
cat /proc/$(pgrep -f mysqld)/oom_score
cat /proc/$(pgrep -f mysqld)/oom_score_adj

# Set oom_score_adj at runtime, no restart needed (choom from util-linux)
choom -p "$(pgrep -f mysqld)" -n -500

# Configure the same behavior permanently via a systemd override
sudo systemctl edit mysql.service
# Paste into the override editor, see the next code block

sudo systemctl daemon-reload
sudo systemctl restart mysql.service

6. Systemd, cgroup v2, and systemd-oomd

Besides OOMScoreAdjust=, systemd exposes direct access to hard memory limits via cgroup v2: MemoryMax= sets the memory.max limit of a service's cgroup, MemoryHigh= sets a softer throttling threshold at which the kernel starts actively slowing the service down before the hard limit is reached. OOMPolicy= matters too: the default stop cleanly shuts down the service as soon as one of its processes is hit by the kernel OOM killer, while continue lets the service keep running despite an internal kill and kill terminates the entire cgroup immediately.

On top of that, modern distributions often run systemd-oomd, a userspace daemon that does not wait for the hard kernel OOM killer, but intervenes proactively based on Pressure Stall Information (PSI) from /proc/pressure/memory. PSI measures how much time processes spend blocked waiting for memory, and systemd-oomd can terminate a candidate as soon as pressure persists, long before the kernel itself reaches a critical state. This is configured globally in /etc/systemd/oomd.conf and per unit via ManagedOOMMemoryPressure= and ManagedOOMSwap=.


; /etc/systemd/system/mysql.service.d/override.conf
; Lower the OOM score for the database and set a hard memory limit
[Service]
OOMScoreAdjust=-500
MemoryMax=6G
MemoryHigh=5G
OOMPolicy=stop

; Disable systemd-oomd for this unit, the decision stays with
; the kernel OOM killer instead of the PSI-based daemon
ManagedOOMMemoryPressure=auto
ManagedOOMSwap=auto

7. Docker and Kubernetes: setting memory limits correctly

Containers get their memory limit through cgroups as well, in Docker via the --memory flag or deploy.resources.limits.memory in a compose file, in Kubernetes via resources.limits.memory in the pod manifest. When a container hits this limit, the OOM killer fires inside the container's cgroup, kills a process there, usually PID 1 of the container, and the container exits with code 137. This becomes visible via docker inspect in the OOMKilled field, or in Kubernetes via kubectl describe pod with the last state showing OOMKilled.

A common mistake in Kubernetes manifests: a limit without a matching request, which causes the scheduler to misjudge actual utilization and pods get unexpectedly evicted or OOM-killed under load. A more realistic requests.memory close to typical consumption combined with a limits.memory with a reasonable buffer for load spikes works better. Behavior similar to memory.high can currently only be approximated in Kubernetes via QoS classes and LimitRange objects, a native soft throttling threshold like the one systemd offers is still missing at the pod level.


# docker-compose.yml: memory limit for a PHP-FPM service
services:
  php-fpm:
    image: php:8.4-fpm
    deploy:
      resources:
        limits:
          memory: 768M
        reservations:
          memory: 384M
    # Once 768M is reached, the cgroup OOM killer kills a process
    # inside this container, exit code 137
    restart: unless-stopped

8. Prevention: swap, PSI monitoring, and alerting

The most effective prevention is realistic capacity planning: enough RAM for the expected peak load, a moderate swap volume as a safety buffer rather than as primary storage, and a low vm.swappiness on servers where latency matters more than maximum cache utilization. Compressed swap via zram significantly reduces the I/O cost of paging out compared to classic file- or partition-based swap, and buys the system valuable additional reaction time during short load spikes before the OOM killer even becomes a consideration.

For proactive monitoring, it is worth regularly checking /proc/pressure/memory: if the avg60 value on the full line stays elevated above low single-digit percentages, memory pressure often announces itself minutes to hours before the actual OOM kill. The node_exporter for Prometheus exports these PSI metrics directly, so alerting rules can be defined that fire before the kernel even kills a process, instead of only reconstructing what happened from the logs afterward.


{
  "_TRANSPORT": "kernel",
  "PRIORITY": "0",
  "SYSLOG_IDENTIFIER": "kernel",
  "MESSAGE": "Out of memory: Killed process 18422 (mysqld) total-vm:4823112kB, anon-rss:3912456kB, file-rss:2048kB, shmem-rss:0kB, UID:112 pgtables:8420kB oom_score_adj:0",
  "__REALTIME_TIMESTAMP": "1783825462123456"
}

9. OOM killer configuration compared

Whether an OOM event unfolds in a controlled, traceable way or hits the wrong service almost always comes down to a handful of configuration decisions. The following overview compares the most common insecure defaults against the recommended configuration.

Area Insecure / Default Recommended Configuration Benefit
Overcommit mode overcommit_memory=1 unlimited overcommit_memory=2 + overcommit_ratio Memory commitments stay traceable
Critical services no oom_score_adj set OOMScoreAdjust=-500 for DB/cache Database does not die first
Container limits limits without requests requests close to real usage + limits with buffer Fewer unexpected evictions
Swap configuration no swap, no buffer zram + moderate swappiness Time buffer during short spikes
Monitoring reactive only, via dmesg after the fact PSI metrics + alerting before the kill Catch incidents before the kill

Most incidents in practice do not come from a single wrong setting, but from the absence of any deliberate configuration at all: default overcommit, no memory limit, no oom_score_adj, no monitoring. Working through these five levers once per server or cluster substantially reduces the likelihood of a surprise production outage caused by the OOM killer, without having to buy any additional hardware.

Mironsoft

Server administration, memory tuning, and incident analysis for Linux infrastructure

Unexplained process deaths caused by the OOM killer?

We analyze your OOM kill logs, identify the actual root cause of the memory pressure, and configure oom_score_adj, cgroup limits, and monitoring so that critical services like your database and cache stay protected.

Incident analysis

Evaluating dmesg and journalctl to reconstruct the exact cause of an OOM event

Memory tuning

Configuring oom_score_adj, cgroup v2 limits, and systemd-oomd for critical services

Proactive monitoring

Setting up PSI-based alerting before the next OOM kill hits production

10. Summary

The OOM killer is not a random event, it follows a clear, traceable logic: it only steps in once reclaim and swap-out are no longer sufficient, and it picks the process with the highest badness score based on its share of total system memory. This selection can be influenced deliberately via oom_score_adj, without changing overall memory consumption. In dmesg and journalctl, every OOM kill leaves behind a complete diagnostic table showing exactly why that particular process was picked and how much memory its competitors were holding at that moment.

The problem is not solved sustainably by shielding a single process, but through a combination of realistic per-service cgroup limits, moderate swap as a time buffer, and PSI-based monitoring that detects memory pressure before the kernel ever has to kill a process. Establishing these building blocks consistently across production servers turns the OOM killer from a surprising source of failures into a predictable, manageable safety net.

Understanding the OOM Killer and Avoiding Memory Problems, the essentials at a glance

Trigger

Only once reclaim, swap, and compaction are no longer enough does the OOM killer get invoked from the page allocator.

Victim selection

The badness score in /proc/PID/oom_score is based on memory share, adjustable via oom_score_adj.

Diagnosis

dmesg -T | grep -i "killed process" and journalctl -k reveal the full kill table.

Prevention

cgroup limits, zram swap, and PSI monitoring via systemd-oomd prevent uncontrolled kills.

11. FAQ: The OOM Killer on Linux

1What is the OOM killer and when does it intervene?
A kernel mechanism that forcibly terminates a process when a memory request can no longer be satisfied despite reclaim and swap-out, preventing a complete system freeze.
2How does the kernel decide which process gets killed?
Via a badness score in /proc/PID/oom_score, roughly proportional to the process's share of total system memory. The highest score gets terminated.
3How do I find out if a process was killed by OOM?
With dmesg -T | grep -i "killed process" or journalctl -k --since "-24h" | grep -i oom. Shows process name, PID, and memory usage at kill time.
4What does oom_score_adj actually do?
Shifts the badness score within -1000 to 1000. Negative values lower kill probability, -1000 excludes entirely, positive values raise it.
5How do I protect my database from the OOM killer?
Via OOMScoreAdjust=-500 or similar in the systemd unit, combined with a realistic MemoryMax. Full protection just shifts the risk to the next process.
6Difference between the kernel OOM killer and systemd-oomd?
The kernel only reacts once an allocation actually fails. systemd-oomd intervenes proactively based on PSI, often before the kernel turns critical.
7Why is my Docker container terminated with exit code 137?
137 means 128 plus SIGKILL. The container hit its cgroup limit and the OOM killer killed a process inside it. docker inspect shows OOMKilled: true.
8Does more swap help against OOM kills?
Swap buys more time but does not fix structural problems. With persistent RAM shortage, heavy swap just causes noticeable latency instead of a fast kill.
9What does vm.overcommit_memory mean?
Controls how strictly memory commitments are checked against available memory. For production servers, mode 2 with a matching overcommit_ratio is usually most traceable.
10Can I disable the OOM killer entirely?
Technically possible, not recommended in practice. Without the OOM killer, real memory exhaustion leads to a frozen or crashing system instead of a controlled kill.