Memory Pressure, Swap and the OOM Killer
When processes want more memory than the machine has, the kernel reclaims: it drops clean page-cache pages, writes dirty ones back, pushes cold anonymous pages to swap, and — if the working sets still do not fit — kills a process; "free memory" was never the number that mattered.
The problem
8 GB of RAM, 12 GB of wishes
Virtual memory lets processes reserve far more than exists (Why Virtual Memory?), and even what they *touch* can exceed RAM, because a page that is present now can be made not-present later and brought back on a fault (Page Faults). The kernel’s job under pressure is to choose which pages stay: the ones being used now — each process’s working set — and to evict the rest. If the combined working sets fit in RAM, the machine runs at full speed while the sum of everything touched is 12 GB. If they do not, every evicted page is needed again soon, and the machine thrashes.
Every page in RAM belongs to one of two classes, and the class decides what eviction costs. File-backed pages — program code, shared libraries, and the page cache holding file contents — have a copy on disk already. A clean one can be dropped for free and re-read on demand; a dirty one (a file write not yet flushed) must be written back first. Anonymous pages — heaps, stacks, anything from malloc or anonymous mmap — have no file behind them. The only way to evict one is to write it to swap (a partition or file), and if there is no swap they cannot be evicted at all.
- Working set: the pages a process touches in a window of time; the only thing that has to be in RAM.
- File-backed clean: free to drop. File-backed dirty: write back, then drop. Anonymous: write to swap, then drop — or never, without swap.
- The page cache is the largest reclaimable pool on most machines, and the reason "free" is small on a healthy system.
Reclaim: LRU lists and the working set
Linux keeps every page on one of a few lists per memory zone: active and inactive, each split into file and anonymous. New pages enter inactive; a page referenced again while on inactive (seen via the PTE accessed bit, Paging) is promoted to active; pages age from active back to inactive as reclaim scans them. Reclaim evicts from the tail of the inactive lists — an approximation of LRU over the whole machine, in the same sense as the LRU Cache: recency as a stand-in for the working set, with the same failure when a one-pass scan (a backup reading 50 GB) floods the inactive list and pushes hot pages out. Linux mitigates that with a workingset shadow-entry mechanism that detects refaults and, since 5.x, a multi-generational LRU (MGLRU) that tracks age more finely.
Reclaim runs at two urgency levels. When free memory drops below a per-zone low watermark, the kswapd kernel thread wakes and reclaims in the background until the high watermark is restored; the machine stays responsive. If allocations outrun kswapd and free memory hits the min watermark, the allocating process itself enters direct reclaim — it stalls inside its own malloc or page fault, scanning lists and writing pages out — which is where latency spikes come from under pressure. vm.swappiness (0–200, default 60) biases reclaim between file pages and anonymous pages; it does not mean "how eager to swap" in any simple sense.
Swap and thrashing
Swap is not a failure mode; it is where cold anonymous memory belongs. A daemon’s initialisation data, an idle tab’s heap, the part of a long-running process that was used once at startup — paging those out makes room for page cache that is actually hot, and a machine with swap and a small swap usage is doing exactly what it should. The pathology is swap traffic, not swap *usage*: pages going out and coming back in continuously because the working sets do not fit.
That is thrashing. Each process runs a few instructions, faults, sleeps for the ~100 µs to 10 ms it takes to page in, runs a few more, faults again; the CPU sits mostly idle in I/O wait while the disk saturates, and nothing makes progress. Load average climbs (processes waiting on I/O are counted), si/so in vmstat are continuously non-zero, %wa is high, and interactive response goes from milliseconds to seconds. The only cures are less demand (kill or shrink something) or more RAM; tuning does not make working sets smaller.
$ vmstat 1 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 3 14 6.9G 112M 2M 180M 41200 38900 44100 39200 6100 9800 4 9 2 85 0 2 16 7.0G 108M 2M 175M 40800 42100 43600 42400 6300 9900 3 8 1 88 0 # si/so ≈ 40 MB/s each way, continuously; b = 14 processes blocked on I/O; wa = 85%: thrashing
The OOM killer and overcommit
Reclaim can fail: no file pages left to drop, no swap (or swap full), and an allocation that must be satisfied. Linux then invokes the OOM killer, which picks the process with the highest oom_score — roughly its share of RAM plus swap, adjusted by oom_score_adj (−1000 exempts, +1000 volunteers) — and sends it SIGKILL. The choice is a heuristic, and it is frequently the wrong one from the operator’s point of view: the largest process is often the database you were trying to protect. dmesg records the decision in detail; systemd and Kubernetes record OOMKilled and exit code 137.
The killer exists because of overcommit. With vm.overcommit_memory = 0 (the default heuristic) the kernel lets processes reserve more than RAM + swap on the theory that they will not touch it all; 1 always allows; 2 refuses any reservation beyond swap + overcommit_ratio × RAM, which turns OOM into malloc returning NULL — deterministic, and the choice of systems that would rather fail a request than lose a process. Windows works in mode 2 by default: commit charge is enforced and there is no OOM killer, so a VirtualAlloc(MEM_COMMIT) beyond the limit fails immediately.
Containers make this local. A cgroup with memory.max = 2G gets its own reclaim and its own OOM killer: when the group’s usage hits the limit, reclaim runs inside the group, and if it fails a process *in the group* is killed, regardless of how much free memory the host has. The page cache a containerised process uses counts against the group too, which is the answer to "my container was OOM-killed at 2 GB while top showed 500 MB RSS" — see Containers Are Processes With the Kernel’s View Narrowed.
$ dmesg -T | grep -A3 'Out of memory' [Tue Aug 25 03:12:41 2026] Out of memory: Killed process 41217 (postgres) total-vm:18432100kB, anon-rss:7911040kB, file-rss:2048kB, shmem-rss:1310720kB, UID:999 pgtables:16784kB oom_score_adj:0 [Tue Aug 25 03:12:41 2026] oom_reaper: reaped process 41217 (postgres), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Why "free memory" is misleading
A healthy Linux box shows almost no free memory, because unused RAM is wasted RAM and the kernel fills it with page cache. free -m prints free (truly unused), buff/cache (page cache, most of it reclaimable) and — since kernel 3.14 — available, the kernel’s estimate of how much can be allocated without swapping, counting reclaimable cache. Available is the number to alert on. Likewise per process, RSS counts shared pages in every process that maps them (ten Chrome renderers "using" 300 MB each share most of it); PSS (proportional set size, in /proc/<pid>/smaps_rollup) divides shared pages by the number of sharers and is what adds up to the real total.
The question to ask is never "how much is free" but "is anything waiting for memory": major faults per second, si/so, direct-reclaim stalls (/proc/pressure/memory on kernels with PSI reports the fraction of time tasks were stalled on memory), and kswapd CPU. A machine at 2% free with zero swap traffic and PSI at 0 is fine; a machine at 20% free with a process in direct reclaim every second is not.
$ free -m
total used free shared buff/cache available
Mem: 15911 4207 173 412 11531 11201
Swap: 4095 618 3477
$ cat /proc/pressure/memory
some avg10=0.00 avg60=0.00 avg300=0.00 total=1183304
full avg10=0.00 avg60=0.00 avg300=0.00 total=402117 # no task currently stalled on memoryKey points
- Only working sets must fit in RAM; the sum of touched memory can exceed it as long as the cold part is evictable.
- File-backed pages can be dropped (clean) or written back then dropped (dirty); anonymous pages can only leave via swap.
- Reclaim approximates LRU over active/inactive lists; kswapd reclaims in the background, direct reclaim stalls the allocating process.
- Swap usage is healthy; continuous swap traffic (
si/so) with high%wais thrashing, and only less demand or more RAM cures it. - When reclaim fails Linux kills the highest oom_score process; Windows and overcommit mode 2 refuse the allocation instead. Cgroups get their own reclaim and killer.
- "free" is meaningless on a healthy system; watch
available, major faults, swap traffic and PSI.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why does the kernel use all my RAM for cache?
Because an idle frame does nothing, while a cached file page saves a 100 µs–10 ms read. Cache is evicted the instant a process needs the frame, so it costs nothing to keep.
▸Why have swap at all on a machine with plenty of RAM?
To let cold anonymous pages leave so that hot file pages can stay. Without swap, a process’s untouched startup data is pinned forever and the page cache is what gets squeezed.
▸Why does Linux kill a process instead of failing malloc?
Because it promised memory it did not have (overcommit), and by the time the promise is called in there is no allocation to fail — the process is touching a page it was already given. Mode 2 makes the promise strict and moves the failure back to malloc.
▸Why was my container killed with free memory on the host?
Its cgroup has its own limit, its own reclaim and its own OOM killer, and page cache it touched counts against the limit. Host free memory is irrelevant to the decision.
Memory pressure
$ free -m
total used free shared buff/cache available
Mem: 8192 3584 512 0 4096 4480
Swap: 2048 0 2048How it fails
What the failure looks like from inside real software.
- Nightly backup reads 50 GB and evicts the database’s working set: the next morning’s queries take major faults for an hour;
posix_fadvise(DONTNEED)or cgroup limits on the backup fix it. - The database was the largest process and the OOM killer chose it: set
oom_score_adj = -1000on it and let the sidecar die instead — or switch to overcommit mode 2. - A container at its memory limit spends its CPU in reclaim before finally being killed: throughput fell 80% for ten minutes with no error in the logs; PSI in the cgroup would have shown it.
- A Java service with
-Xmxat 90% of the container limit: heap plus metaspace plus thread stacks plus page cache exceed the limit —OOMKilled, exit 137, no Java exception. - A monitoring alert on "free memory below 10%" that fires on every healthy host and never on the thrashing one; alert on
availableand swap traffic instead. - Swap disabled "for performance": anonymous memory can never leave, the page cache shrinks to nothing, and every file read becomes a disk read.