Memory Overcommit and Accounting
Linux routinely hands out more virtual memory than it has physical RAM plus swap to back it. This is memory overcommit, and it is the default, deliberate policy — not a bug. The justification is that programs habitually reserve far more address space than they ever touch: a
malloc()of a gigabyte, afork()that duplicates a multi-gigabyte address space via copy-on-write, a sparse array mapped withmmap. Backing every promised page with a real page at allocation time would waste enormous amounts of memory on pages that are never written. Instead the kernel commits lazily: the page-fault handler allocates a physical page only on first touch. Overcommit accounting (vm.overcommit_memory, theCommitted_ASandCommitLimitfields in/proc/meminfo) is the bookkeeping that decides, at allocation time, whether to believe a new promise. When overcommit is on and the promises eventually come due faster than RAM can satisfy them, the OOM killer is the mechanism that pays the debt. This note pins every behavior to the Linux 6.12 and 6.18 long-term-support (LTS) kernels.
Mental Model — Promises vs. Pages
Think of physical memory as a bank and each mmap/brk/fork as a line of credit, not a withdrawal. The kernel tracks the total credit it has extended (Committed_AS) and, in the strict mode, refuses to extend more than a ceiling (CommitLimit). Most of the time the credit is never fully drawn — which is exactly why the bank can safely promise more than it holds. Overcommit policy is the bank’s lending strategy.
flowchart TB REQ["Process requests address space<br/>mmap / brk / fork / mremap"] REQ --> MODE{vm.overcommit_memory} MODE -->|"1 = ALWAYS"| OK1["Always succeed<br/>(no accounting check)"] MODE -->|"0 = GUESS (default)"| GUESS{"request ><br/>RAM + swap?"} GUESS -->|no| OK0["Succeed"] GUESS -->|"yes (wild alloc)"| FAIL0["-ENOMEM"] MODE -->|"2 = NEVER"| LIMIT{"Committed_AS + req<br/>> CommitLimit?"} LIMIT -->|no| OK2["Succeed, charge Committed_AS"] LIMIT -->|yes| FAIL2["-ENOMEM (early, no OOM)"] OK0 --> TOUCH["Pages faulted in on first touch<br/>(demand paging)"] OK1 --> TOUCH OK2 --> TOUCH TOUCH -->|"RAM exhausted under modes 0/1"| OOM["OOM killer fires"]
The three overcommit modes and where the bill comes due. What it shows: mode 1 (always) skips accounting entirely; mode 0 (the default heuristic) only rejects a single allocation larger than all RAM+swap; mode 2 (never) enforces a hard CommitLimit and fails allocations early with -ENOMEM rather than risk an OOM kill later. The insight to take: modes 0 and 1 push the failure to fault time (where the only recourse is the OOM killer), while mode 2 pushes it to allocation time (where the program gets a clean error it can handle). Overcommit is the choice between optimistic lazy backing and pessimistic up-front guarantees.
Why Linux Overcommits
The case for overcommit is empirical: real workloads over-reserve. Several common patterns make eager backing wasteful or impossible:
mallocover-allocation.glibc’s allocator grows the heap withbrk(2)/sbrk(2)and serves allocations larger thanMMAP_THRESHOLD(128 KB by default) with a private anonymousmmap(2)(malloc(3)); it requests memory from the kernel in chunks and parcels it out, so most programs reserve more than they ever free or touch.fork()and copy-on-write.fork()conceptually duplicates the parent’s entire address space, but the kernel marks the pages read-only and shares them; a private copy is made only on write. A 4 GiB process that forks and immediatelyexecs a tiny program would, under eager accounting, momentarily “owe” 8 GiB it never uses. The overcommit-accounting doc captures this rule precisely: aPRIVATE WRITABLEanonymous mapping costs “size of mapping per instance,” but read-only and shared file mappings cost 0 because the file is the backing store (overcommit-accounting.rst, v6.12).- Sparse data structures. Scientific code mapping a huge sparse array relies on the “virtual memory consisting almost entirely of zero pages” — the kernel doc names this as the motivating use case for mode 1 (
overcommit-accounting.rst).
Backing all of that eagerly would force you to provision RAM+swap for the sum of all promises rather than the peak of all touches, typically a multiple more memory for no benefit. Overcommit trades a small tail risk (running out of physical memory and invoking the OOM killer) for a large, constant memory saving.
The fork() case is worth drawing, because it is the one where the commit ledger and physical reality diverge most violently, and it is the reason the most-cited real-world overcommit tuning advice exists at all:
flowchart TB subgraph BEFORE["Before fork: one 4 GiB Redis process"] P1["parent mm<br/>private writable anon: 4 GiB<br/><b>Committed_AS charge: 4 GiB</b><br/>RSS: 4 GiB"] end BEFORE --> FORK["fork()<br/>dup_mmap() walks every VMA"] FORK --> CHECK{"for each VMA with VM_ACCOUNT:<br/>security_vm_enough_memory_mm()<br/>-> __vm_enough_memory()"} CHECK -->|"mode 0: is this ONE vma<br/>> RAM+swap? no"| PASS["charge accepted"] CHECK -->|"mode 2: would<br/>Committed_AS + 4 GiB<br/>> CommitLimit? maybe"| DENY["<b>fork() fails with -ENOMEM</b><br/>Redis: 'Background saving<br/>fails with a fork() error'"] PASS --> AFTER subgraph AFTER["After fork: ledger says 8 GiB, hardware says 4 GiB"] direction TB A1["parent mm: 4 GiB charged"] A2["child mm: 4 GiB charged"] A3["<b>Committed_AS: 8 GiB</b>"] A4["<b>Actual physical pages: still 4 GiB</b><br/>every page shared read-only (COW)"] end AFTER --> EXIT["child writes almost nothing,<br/>snapshots, exits<br/>-> Committed_AS falls back to 4 GiB"]
What fork() does to the commit ledger versus what it does to physical memory. What it shows: dup_mmap() charges the full size of every accountable VMA a second time, so Committed_AS doubles instantly, while copy-on-write means not one additional physical page is allocated. The insight to take: the doubled charge is pure bookkeeping about a possibility that, for a snapshotting child, never materializes. Mode 2 has no way to know that and must refuse; mode 0 does not look at the total and lets it through. This diagram is the mechanism behind the Redis guidance discussed later in this note — and it shows why the advice is vm.overcommit_memory = 1 rather than “add more swap.”
The Three Modes — vm.overcommit_memory
The policy is selected by the sysctl vm.overcommit_memory, an integer 0/1/2. The authoritative description is the kernel’s own overcommit doc (overcommit-accounting.rst, v6.12):
0 — Heuristic overcommit handling. Obvious overcommits of address space are refused. Used for a typical system. … This is the default. 1 — Always overcommit. Appropriate for some scientific applications. … 2 — Don’t overcommit. The total address space commit for the system is not permitted to exceed swap + a configurable amount (default is 50%) of physical RAM.
All three live in one decision function, __vm_enough_memory() in mm/util.c, called on every accounted allocation. Here it is verbatim (mm/util.c, v6.12):
int __vm_enough_memory(struct mm_struct *mm, long pages, int cap_sys_admin)
{
long allowed;
unsigned long bytes_failed;
vm_acct_memory(pages);
/*
* Sometimes we want to use more memory than we have
*/
if (sysctl_overcommit_memory == OVERCOMMIT_ALWAYS)
return 0;
if (sysctl_overcommit_memory == OVERCOMMIT_GUESS) {
if (pages > totalram_pages() + total_swap_pages)
goto error;
return 0;
}
allowed = vm_commit_limit();
/*
* Reserve some for root
*/
if (!cap_sys_admin)
allowed -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10);
/*
* Don't let a single process grow so big a user can't recover
*/
if (mm) {
long reserve = sysctl_user_reserve_kbytes >> (PAGE_SHIFT - 10);
allowed -= min_t(long, mm->total_vm / 32, reserve);
}
if (percpu_counter_read_positive(&vm_committed_as) < allowed)
return 0;
error:
bytes_failed = pages << PAGE_SHIFT;
pr_warn_ratelimited("%s: pid: %d, comm: %s, bytes: %lu not enough memory for the allocation\n",
__func__, current->pid, current->comm, bytes_failed);
vm_unacct_memory(pages);
return -ENOMEM;
}Reading the three branches:
-
OVERCOMMIT_ALWAYS(mode 1) returns0(success) unconditionally after charging the accounting counter viavm_acct_memory(pages). No limit is consulted; the allocation always succeeds. The only way to fail is to physically run out of pages at fault time, which then summons the OOM killer. -
OVERCOMMIT_GUESS(mode 0, the default) is the heuristic, and it is far simpler than its name suggests: it fails only if a single allocation request (pages) exceeds all of RAM plus swap (totalram_pages() + total_swap_pages). Anything smaller succeeds. This is the literal meaning of the doc’s “obvious overcommits… refused” andvm.rst’s “rejects obvious overcommits” (vm.rst, v6.12): mode 0 catches only the catastrophically-wild single allocation (a corrupted size, an accidental1 << 60), and lets all normal over-reservation through. It does not track cumulative commitment against a ceiling — that is mode 2’s job. Two independent primary sources (the prose doc and the code) agree on exactly this, so it is not a “guess” in the sense of being fuzzy; it is a deliberately permissive single-allocation sanity check. -
OVERCOMMIT_NEVER(mode 2) is the only mode that enforces a cumulative ceiling. It computesallowed = vm_commit_limit()(theCommitLimit, below), subtracts two reserves, and refuses the allocation if the system’s running total of committed address space (vm_committed_as) plus the new request would exceedallowed. On refusal it logs a rate-limited warning, un-accounts the pages, and returns-ENOMEM— a clean error the calling program sees as a failedmmap/brk. Crucially this happens at allocation time, so a well-written program can handle it; there is no later OOM surprise (in the common case).The two reserves protect against pathological exhaustion:
sysctl_admin_reserve_kbytes(vm.admin_reserve_kbytes) is withheld from non-privileged callers (!cap_sys_admin) so that a root recovery shell can still allocate after userspace has filled memory.sysctl_user_reserve_kbytes(vm.user_reserve_kbytes), capped atmm->total_vm / 32, keeps a single runaway process from consuming so much that the user cannot even runkillto recover.
In the v6.18 LTS source, __vm_enough_memory() and the mode constants are unchanged in behavior, and the default remains OVERCOMMIT_GUESS (int sysctl_overcommit_memory __read_mostly = OVERCOMMIT_GUESS; in mm/util.c, v6.18). Everything in this section holds for both LTS branches as of 2026.
The Three Modes as an Admission Rule
Stated as the exact predicate each mode applies — pages is the size of this request in pages, and every symbol below is read straight out of __vm_enough_memory():
vm.overcommit_memory | Constant | Admission rule actually evaluated | Is a cumulative total consulted? | Where it can fail |
|---|---|---|---|---|
| 0 (default) | OVERCOMMIT_GUESS | pages > totalram_pages() + total_swap_pages ⇒ -ENOMEM; otherwise succeed | No — only this one request’s size | fault time (OOM killer), plus the single-request sanity check |
| 1 | OVERCOMMIT_ALWAYS | none — return 0 immediately after charging | No | fault time only (OOM killer) |
| 2 | OVERCOMMIT_NEVER | vm_committed_as + pages > vm_commit_limit() − admin_reserve − min(mm->total_vm/32, user_reserve) ⇒ -ENOMEM | Yes — vm_committed_as, the system-wide running total | allocation time (mmap/brk/mremap/fork return -ENOMEM) |
The three modes reduced to the predicate each one evaluates. What it shows: modes 0 and 1 differ from each other far less than either differs from mode 2. Mode 0’s check does not involve CommitLimit, Committed_AS, overcommit_ratio, or any accumulated state whatsoever — it compares one request against all the memory in the machine. Mode 1 removes even that. The insight to take: “heuristic” (mode 0) sounds like it means “makes a judgement call about whether the system can cope.” It does not. It is a single arithmetic comparison that catches a corrupted size argument and nothing else. If you want the cumulative accounting that CommitLimit describes to be enforced, mode 2 is the only mode that enforces it — and switching to it is a policy change with real consequences, not a tuning tweak.
Two details in that mode-2 row deserve unpacking because they are easy to miss and both make the effective ceiling lower than the advertised CommitLimit. The reserves are absolute byte counts, not percentages of the limit. Their compile-time initializers in mm/util.c are (mm/util.c:908-909, v6.12):
unsigned long sysctl_user_reserve_kbytes __read_mostly = 1UL << 17; /* 128MB */
unsigned long sysctl_admin_reserve_kbytes __read_mostly = 1UL << 13; /* 8MB */1UL << 17 is 131,072 KiB = 128 MiB and 1UL << 13 is 8,192 KiB = 8 MiB, matching the inline comments. But those initializers are not the effective defaults — they are placeholders that get overwritten during boot. Two subsys_initcalls in a different file recompute both values from the amount of free memory actually present (mm/mmap.c:2206-2236, v6.12):
/*
* The default value is min(3% of free memory, 128MB)
* 128MB is enough to recover with sshd/login, bash, and top/kill.
*/
static int init_user_reserve(void)
{
unsigned long free_kbytes = K(global_zone_page_state(NR_FREE_PAGES));
sysctl_user_reserve_kbytes = min(free_kbytes / 32, SZ_128K);
return 0;
}
subsys_initcall(init_user_reserve);
/*
* Systems with more than 256MB will reserve 8MB, enough to recover
* with sshd, bash, and top in OVERCOMMIT_GUESS. Smaller systems will
* only reserve 3% of free pages by default.
*/
static int init_admin_reserve(void)
{
unsigned long free_kbytes = K(global_zone_page_state(NR_FREE_PAGES));
sysctl_admin_reserve_kbytes = min(free_kbytes / 32, SZ_8K);
return 0;
}
subsys_initcall(init_admin_reserve);So the real rule is min(free_kbytes / 32, cap), where free_kbytes / 32 is 1/32 = 3.125% of free memory (which both the kernel comments and vm.rst round to “3%”) and the cap is SZ_128K/SZ_8K. Those two constants are a genuine readability trap: SZ_128K is the number 131,072 and SZ_8K is 8,192, and because the variable’s unit is kilobytes they mean 128 MiB and 8 MiB, not 128 KiB and 8 KiB. On any machine with more than ~256 MiB free at boot the min() clamps to the caps, which is why the clamped values are what you almost always observe — confirmed on the measurement machine:
$ sysctl vm.admin_reserve_kbytes vm.user_reserve_kbytes
vm.admin_reserve_kbytes = 8192
vm.user_reserve_kbytes = 1310728,192 KiB and 131,072 KiB — the caps, as expected on a 125 GiB box. The lesson generalizes: vm.rst’s “defaults to min(3% of free pages, 8MB)” is correct and the C initializer is misleading. Reading only mm/util.c would have you reporting a flat 8 MiB default that is actually only what a large machine happens to converge on; on a 128 MiB embedded system the admin reserve would be ~4 MiB instead. There is even a memory-hotplug notifier, reserve_mem_notifier() at mm/mmap.c:2256, that re-runs both initializers when memory is added or removed — but only if the value still looks like a default, with the comment “If memory is added and the reserves have been eliminated or increased above the default max, then we’ll trust the admin.”
How the two reserves are then applied differs. The admin reserve is subtracted for any caller without CAP_SYS_ADMIN, so an unprivileged process in mode 2 sees a ceiling 8 MiB below CommitLimit; vm.rst advises that “systems running under overcommit ‘never’ should increase this to account for the full Virtual Memory Size of programs used to recover. Otherwise, root may not be able to log in to recover the system.” The user reserve is subtracted as min_t(long, mm->total_vm / 32, reserve) — note it is the minimum of the two, so a small process is charged total_vm/32 (proportional, and tiny) while only a process whose address space exceeds 4 GiB is charged the full 128 MiB. This is a deliberately gentle brake: it scales with how big the offender already is.
CommitLimit — How the Ceiling Is Computed
CommitLimit is the mode-2 ceiling, computed by vm_commit_limit() (mm/util.c, v6.12):
unsigned long vm_commit_limit(void)
{
unsigned long allowed;
if (sysctl_overcommit_kbytes)
allowed = sysctl_overcommit_kbytes >> (PAGE_SHIFT - 10);
else
allowed = ((totalram_pages() - hugetlb_total_pages())
* sysctl_overcommit_ratio / 100);
allowed += total_swap_pages;
return allowed;
}The ceiling is therefore swap + a slice of RAM, where the slice is specified one of two mutually-exclusive ways:
vm.overcommit_kbytes— an absolute amount of RAM, in kibibytes (converted to pages by>> (PAGE_SHIFT - 10), i.e. divide by 4 since a page is 4 KiB = 2^2 KiB). If set non-zero, it wins.vm.overcommit_ratio— a percentage of physical RAM (minus huge-page reservations, which are managed separately by hugetlbfs). The default is 50, matching the doc’s “default is 50%”. So on a 32 GiB machine with no swap, the defaultCommitLimitis 16 GiB.
flowchart TD START["vm_commit_limit() called<br/>(on every /proc/meminfo read,<br/>and on every mode-2 allocation)"] START --> KB{"sysctl_overcommit_kbytes<br/>non-zero?"} KB -->|"yes — absolute mode"| ABS["allowed = overcommit_kbytes<br/>>> (PAGE_SHIFT - 10)<br/>i.e. kBytes / 4 = pages"] KB -->|"no — percentage mode<br/>(the default)"| PCT["allowed = (totalram_pages()<br/>− hugetlb_total_pages())<br/>× overcommit_ratio / 100"] ABS --> SWAP["allowed += total_swap_pages"] PCT --> SWAP SWAP --> OUT["<b>CommitLimit</b>"] OUT --> USE{"vm.overcommit_memory"} USE -->|"0 or 1"| INFO["printed in /proc/meminfo<br/><b>and then ignored</b>"] USE -->|"2"| ENF["minus admin_reserve (8 MiB)<br/>minus min(total_vm/32, 128 MiB)<br/>= the real ceiling enforced"] MEAS["This machine:<br/>0.5 × 131,150,216 + 8,388,604<br/>= 73,963,712 kB"] -.->|"matches reported<br/>CommitLimit exactly"| OUT
How CommitLimit is computed and what happens to it afterwards. What it shows: swap is always added at full value regardless of which branch computed the RAM slice — so overcommit_ratio throttles only the RAM contribution, never the swap contribution. The insight to take: two consequences follow. Adding swap raises CommitLimit one-for-one, which is the cheapest way to make mode 2 usable; and a swapless machine in mode 2 with the default ratio can only ever commit half its RAM, which is why mode 2 is almost always paired with a raised overcommit_ratio or with swap. The dotted arrow is this machine’s numbers substituted into the percentage branch, matching the reported value to the kilobyte.
These two are counterparts: setting one zeroes the other (reading the inactive one returns 0), as vm.rst notes (vm.rst, v6.12). Both overcommit_ratio and overcommit_kbytes only take effect in mode 2 — in modes 0 and 1 they are computed and displayed but never consulted by the allocation path.
Reading the State — /proc/meminfo
Two /proc/meminfo fields expose the accounting, emitted by fs/proc/meminfo.c (v6.12):
committed = vm_memory_committed();
...
show_val_kb(m, "CommitLimit: ", vm_commit_limit());
show_val_kb(m, "Committed_AS: ", committed);CommitLimitis exactlyvm_commit_limit()rendered in kibibytes — the mode-2 ceiling. It is shown regardless of mode, which is the single most common source of confusion: seeing aCommitLimitdoes not mean the system is enforcing it. In the default mode 0,CommitLimitis purely informational. It is the threshold that would apply if you switched to mode 2.Committed_AS(“committed address space”) is the running totalvm_committed_as— the sum of all memory the kernel has promised to back, across every process, computed by the accounting rules inovercommit-accounting.rst(private-writable anonymous = full size, shared/read-only file = 0, and so on). WhenCommitted_ASexceedsCommitLimit, the system is overcommitted — fine in modes 0/1, impossible in mode 2.
A worked example: on a host showing MemTotal: 16 GiB, SwapTotal: 4 GiB, default overcommit_ratio = 50, the CommitLimit is 0.5 × 16 + 4 = 12 GiB. A Committed_AS of 20 GiB means programs have reserved 20 GiB of address space — perfectly normal under mode 0 because most of it is untouched COW or sparse mapping; switching to mode 2 at this point would immediately start failing new mallocs.
Which /proc/meminfo Fields Matter, and Which Are Estimates
A surprising amount of confusion about overcommit comes from comparing fields that are not measuring comparable things. The fields that bear on overcommit, with an honest note on each one’s exactness:
| Field | What it is | Exact or estimate? |
|---|---|---|
MemTotal | usable RAM (total physical minus kernel reserved/firmware) | exact, fixed at boot (changes only on memory hotplug) |
SwapTotal | total swap | exact |
CommitLimit | vm_commit_limit() — swap + ratio% × (RAM − hugetlb) | exact, a pure function of three sysctls plus MemTotal. Recomputed on every read |
Committed_AS | vm_memory_committed() — the promise ledger | effectively exact when read here (see below), but not the value the admission check uses |
MemFree | pages on the free lists right now | exact, but nearly useless in isolation — a healthy Linux box keeps MemFree low on purpose, using the rest as page cache |
MemAvailable | si_mem_available() — how much could be handed out without swapping | an estimate, by design. The proc man page calls it “an estimate of how much memory is available for starting new applications, without swapping.” This is the field to watch for real pressure |
HugePages_Total / Hugetlb | huge-page pool | exact — and note Hugetlb is subtracted from the RAM that feeds CommitLimit |
VmallocTotal | size of the kernel’s vmalloc address range | exact but routinely misread — on this machine it reads 34,359,738,367 kB (32 TiB), which is an address-space size, not memory |
The overcommit-relevant /proc/meminfo fields. What it shows: exactly one field on this list is an intentional estimate (MemAvailable), and it is the one that actually answers “is this machine short of memory?” The insight to take: the natural instinct is to compare Committed_AS against MemTotal and panic. The useful comparison is MemAvailable against what you are about to allocate; Committed_AS against CommitLimit only means something if vm.overcommit_memory is 2.
There is a real exactness subtlety in Committed_AS, and it runs in the direction most people would not guess. vm_committed_as is a per-CPU counter, deliberately cache-line isolated because it is written on every accounted allocation on every CPU (mm/util.c):
/*
* Make sure vm_committed_as in one cacheline and not cacheline shared with
* other variables. It can be updated by several CPUs frequently.
*/
struct percpu_counter vm_committed_as ____cacheline_aligned_in_smp;A percpu_counter keeps a small per-CPU delta that is folded into the global total only when it exceeds a batch threshold. That means there are two ways to read it, and the kernel uses different ones in the two places that matter:
/proc/meminfouses the accurate read.vm_memory_committed()callspercpu_counter_sum_positive(), which walks every CPU’s delta and adds them up. The kernel even documents its cost: “for a big platform like a 2S/36C/72T Skylake server, in worst case wherevm_committed_as’s spinlock is under severe contention, the time cost could be about 30~40 microseconds.”- The admission check uses the fast, sloppy read.
__vm_enough_memory()callspercpu_counter_read_positive(), which reads only the folded global total and ignores the unfolded per-CPU deltas. It must — it runs on everymmap, and a 40 µs cross-CPU walk there would be unacceptable.
So the number you read in /proc/meminfo is more accurate than the number mode 2 enforces against. How much less accurate is the enforcement read? The batch size is set by mm_compute_batch() (mm/mm_init.c:164-183, v6.12), and it depends on the overcommit mode:
void mm_compute_batch(int overcommit_policy)
{
u64 memsized_batch;
s32 nr = num_present_cpus();
s32 batch = max_t(s32, nr*2, 32);
unsigned long ram_pages = totalram_pages();
/*
* For policy OVERCOMMIT_NEVER, set batch size to 0.4% of
* (total memory/#cpus), and lift it to 25% for other policies
* to easy the possible lock contention for percpu_counter
* vm_committed_as, while the max limit is INT_MAX
*/
if (overcommit_policy == OVERCOMMIT_NEVER)
memsized_batch = min_t(u64, ram_pages/nr/256, INT_MAX);
else
memsized_batch = min_t(u64, ram_pages/nr/4, INT_MAX);
vm_committed_as_batch = max_t(s32, memsized_batch, batch);
}Working this out for the measurement machine — 32 CPUs, 32,787,554 pages of RAM:
| Mode | Formula | Batch per CPU | Worst-case total skew (batch × nr_cpus) |
|---|---|---|---|
0 or 1 (GUESS/ALWAYS) | ram_pages / 32 / 4 | 256,152 pages ≈ 1,000 MiB | ≈ 31.3 GiB |
2 (NEVER) | ram_pages / 32 / 256 | 4,002 pages ≈ 15.6 MiB | ≈ 0.49 GiB |
The percpu_counter batch size on a 32-CPU / 125 GiB machine, per mode. What it shows: in the default mode the counter is allowed to drift by up to ~31 GiB before all CPUs fold their deltas in — a deliberate trade of accuracy for the elimination of a hot shared cache line. In mode 2 the kernel tightens it by 64× to ~0.5 GiB, because now the counter is load-bearing. The insight to take: the loose batch is safe precisely because modes 0 and 1 never read the total. The kernel is not being sloppy; it is spending accuracy only where accuracy is free.
This is also why switching modes is not a simple store. overcommit_policy_handler() (mm/util.c) does a careful three-step dance, and the comment explains exactly why:
/*
* The deviation of sync_overcommit_as could be big with loose policy
* like OVERCOMMIT_ALWAYS/OVERCOMMIT_GUESS. When changing policy to
* strict OVERCOMMIT_NEVER, we need to reduce the deviation to comply
* with the strict "NEVER", and to avoid possible race condition (even
* though user usually won't too frequently do the switching to policy
* OVERCOMMIT_NEVER), the switch is done in the following order:
* 1. changing the batch
* 2. sync percpu count on each CPU
* 3. switch the policy
*/
if (write) {
...
mm_compute_batch(new_policy);
if (new_policy == OVERCOMMIT_NEVER)
schedule_on_each_cpu(sync_overcommit_as);
sysctl_overcommit_memory = new_policy;
}Tighten the batch first, then schedule_on_each_cpu() a work item that folds every CPU’s pending delta into the global total, and only then flip the policy. Doing it in any other order would mean mode 2’s very first admission decision was made against a total that could be ~31 GiB stale. The two sibling handlers enforce the mutual exclusion noted earlier by brute force: overcommit_ratio_handler() zeroes sysctl_overcommit_kbytes on write, and overcommit_kbytes_handler() zeroes sysctl_overcommit_ratio on write — which is why reading the inactive one returns 0.
Committed_AS Is Not a Memory-Usage Measurement
This is the single thing readers most often get wrong about this subject, and it is worth stating as bluntly as possible: Committed_AS is not an amount of memory in use, and it is not an amount of memory that will be used. It is the sum of address space that could be faulted in without any further permission from the kernel. Nothing about the number tells you whether a single one of those pages has ever been touched.
Because of that, Committed_AS can exceed RAM + swap, and under the default mode 0 it routinely does. That is not a warning sign, an error, or a sign of a leak. It is the expected steady state of a working overcommit system — the gap between Committed_AS and actual resident memory is the benefit overcommit delivers. A system where Committed_AS closely tracked real usage would be a system where overcommit was buying nothing.
Here is that claim measured, not asserted, on the machine used throughout this note — 125 GiB of RAM, 8 GiB of swap, running the stock default mode 0 with nothing unusual loaded:
$ sysctl vm.overcommit_memory vm.overcommit_ratio vm.overcommit_kbytes
vm.overcommit_memory = 0
vm.overcommit_ratio = 50
vm.overcommit_kbytes = 0
$ grep -E 'MemTotal|SwapTotal|CommitLimit|Committed_AS|MemAvailable' /proc/meminfo
MemTotal: 131150216 kB
SwapTotal: 8388604 kB
MemAvailable: 95635448 kB
CommitLimit: 73963712 kB
Committed_AS: 84508764 kBRead those four numbers against each other:
CommitLimitis 73,963,712 kB ≈ 70.5 GiB. Check the arithmetic:overcommit_ratiois 50 andovercommit_kbytesis 0, so the formula is0.5 × MemTotal + SwapTotal=0.5 × 131,150,216 + 8,388,604= 73,963,712 — an exact match to the reported value, which independently confirms both the formula and thathugetlb_total_pages()is zero here (HugePages_Total: 0in/proc/meminfo).Committed_ASis 84,508,764 kB ≈ 80.6 GiB, which is 114% ofCommitLimit— already over the ceiling, at rest, with no unusual workload.- And yet
MemAvailableis 95,635,448 kB ≈ 91 GiB. The machine is not short of memory at all. It has ~91 GiB available while “owing” 80.6 GiB against a 70.5 GiB limit.
Nothing is wrong. In mode 0 the CommitLimit is a number the kernel computes and prints and then never consults. Switching this machine to mode 2 right now would immediately begin failing allocations — not because memory ran out, but because Committed_AS already sits 10 GiB above the ceiling that mode 2 would start enforcing.
flowchart LR subgraph W["What Committed_AS counts"] direction TB A1["private writable anon mapping<br/>(malloc'd but never touched)"] A2["COW pages after fork<br/>(never written by either side)"] A3["MAP_SHARED anon / shmem<br/>(charged at creation)"] A4["grown stack / brk heap"] end subgraph R["What RSS counts"] direction TB B1["pages actually faulted in<br/>and still resident"] end W -->|"charged at mmap/brk/fork time<br/>__vm_enough_memory()"| CAS["Committed_AS<br/>(a promise ledger)"] R -->|"charged at first touch<br/>page fault handler"| RSS["RSS / MemTotal-MemAvailable<br/>(a usage measurement)"] CAS --> GAP["Committed_AS - RSS = untouched promises<br/><b>this gap is the point of overcommit</b>"] RSS --> GAP GAP --> CMP["So: Committed_AS > RAM+swap is NORMAL,<br/>and Committed_AS > CommitLimit is normal in modes 0/1"]
Two separate ledgers, updated at two different moments. What it shows: Committed_AS is incremented by the allocation path (mmap, brk, fork, mremap, mprotect-to-writable, shmem creation) and RSS is incremented by the fault path. Nothing ever reconciles them. The insight to take: there is no arithmetic relationship between the two beyond RSS ≤ Committed_AS for accounted mappings — and even that is violated, in both directions, by mappings that are resident but unaccounted (a MAP_NORESERVE region you touched) and by mappings that are accounted but were swapped out. Treating Committed_AS as “memory in use” and alerting on it is a classic monitoring mistake: it will fire constantly on a perfectly healthy mode-0 machine.
Where the refusal actually happens
Being precise about which mode refuses and where matters because the check lives in exactly one function, reached from a fixed set of call sites. The decision is __vm_enough_memory() in mm/util.c (at v6.12, defined at mm/util.c:1032). It is never called directly by the VM; callers go through the LSM wrapper security_vm_enough_memory_mm(), which is what makes the check hookable by a security module. Every call site in the v6.12 tree:
| Caller | File and line | What it charges |
|---|---|---|
__mmap_region() | mm/mmap.c:1410 | a new private-writable mapping, gated on accountable_mapping() |
do_brk_flags() | mm/mmap.c:1794 | heap growth via brk; the VMA is created with VM_ACCOUNT unconditionally |
acct_stack_growth() | mm/mmap.c:1032 | one stack-expansion step — the “implicit mremap” the kernel doc’s Gotchas section warns about |
insert_vm_struct() | mm/mmap.c:1975 | a VMA inserted outside the normal mmap path, if it carries VM_ACCOUNT |
mprotect_fixup() | mm/mprotect.c:655 | making an existing read-only private mapping writable — the moment a 0-cost mapping becomes a full-cost one |
move_vma() | mm/mremap.c:708 | a relocating mremap |
SYSCALL_DEFINE5(mremap, ...) | mm/mremap.c:1126 | an in-place mremap that grows |
dup_mmap() | kernel/fork.c:688 | each accountable VMA duplicated by fork(). The call is marked /* sic */ in the source, flagging that it charges against oldmm rather than the new mm |
shmem_acct_size() / shmem_acct_blocks() | mm/shmem.c:174, 207 | tmpfs/shmem size and block allocation — “shmfs memory drawn from the same pool,” per the doc |
Every path that can be refused by overcommit accounting, at v6.12. What it shows: eight distinct kernel paths funnel into one if. Two of them are routinely surprising: mprotect_fixup means changing protections can fail with -ENOMEM (turning a PROT_READ private mapping into PROT_READ|PROT_WRITE incurs the full charge at that moment), and shmem_acct_* means writing to a tmpfs file consumes commit budget. The insight to take: in mode 2, -ENOMEM is not confined to mmap and malloc. A program can be refused by mprotect, by mremap, by fork, by growing its own stack, or by write() to /dev/shm — and most programs check the return value of none of those.
The vm_unacct_memory() side is symmetric and mostly silent: exit_mmap() at mm/mmap.c:1959 releases a dying process’s whole charge in one call, and munmap releases per-VMA. This is why Committed_AS falls as processes exit even though no physical memory was necessarily freed at that instant.
What Is Accounted and What Is Not
The rules the kernel documentation states in prose are implemented by one predicate, accountable_mapping() (mm/mmap.c:584-593, v6.12):
/*
* We account for memory if it's a private writeable mapping,
* not hugepages and VM_NORESERVE wasn't set.
*/
static inline bool accountable_mapping(struct file *file, vm_flags_t vm_flags)
{
/*
* hugetlb has its own accounting separate from the core VM
* VM_HUGETLB may not be set yet so we cannot check for that flag.
*/
if (file && is_file_hugepages(file))
return false;
return (vm_flags & (VM_NORESERVE | VM_SHARED | VM_WRITE)) == VM_WRITE;
}That return is worth reading carefully, because it is a three-bit exact-match test, not a series of ORs. It masks out all but three flags and demands the result be exactly VM_WRITE — meaning VM_WRITE set and VM_NORESERVE clear and VM_SHARED clear. Any one of those three conditions failing makes the mapping unaccounted. So:
| Mapping | Accounted against Committed_AS? | Why |
|---|---|---|
MAP_PRIVATE anonymous, writable | Yes — full size | the canonical case: exactly VM_WRITE. “size of mapping per instance” |
MAP_PRIVATE anonymous, PROT_READ only | No | VM_WRITE clear. Doc: “PRIVATE READ-only — 0 cost (but of little use)” |
MAP_PRIVATE file-backed, writable | Yes — full size | private writes go to swap, not the file, so they need backing |
MAP_PRIVATE file-backed, read-only | No | the file is the backing store |
MAP_SHARED file-backed (any protection) | No | VM_SHARED set. Doc: “SHARED or READ-only — 0 cost (the file is the map not swap)” |
MAP_SHARED anonymous (and shmget, /dev/shm, tmpfs) | Yes — full size | not the same as shared file: there is no file to page to, so it is charged — via shmem_acct_size(), not accountable_mapping(). Doc: “For an anonymous or /dev/zero map: SHARED — size of mapping” |
Anything with MAP_NORESERVE (modes 0 and 1) | No | VM_NORESERVE set ⇒ __vm_enough_memory() is never even called |
Anything with MAP_NORESERVE (mode 2) | Yes | VM_NORESERVE is not set in mode 2 — see below |
MAP_DROPPABLE (since v6.11) | No | mmap.c sets VM_NORESERVE for it: “If the pages can be dropped, then it doesn’t make sense to reserve them” |
hugetlbfs / MAP_HUGETLB | No — separate system entirely | accountable_mapping() returns false outright for a hugepage file |
mprotect read-only → writable on a private mapping | Yes, charged at mprotect time | mprotect_fixup() calls the check |
What counts toward Committed_AS and what does not. What it shows: the rule is “private and writable and not opted out,” and the two rows people get backwards are the shared ones — shared file mappings are free, shared anonymous mappings cost full size. The insight to take: the reason is always “is there somewhere other than swap for this page to live?” A shared file mapping can be written back to its file, so it needs no commitment. Shared anonymous memory has no file, so it is as expensive as private anonymous memory despite the word “shared” in its name.
MAP_NORESERVE and hugetlb: two separate inversions
The MAP_NORESERVE handling in mmap.c contains a subtlety that is easy to read backwards (mm/mmap.c:486-493, v6.12):
if (flags & MAP_NORESERVE) {
/* We honor MAP_NORESERVE if allowed to overcommit */
if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
vm_flags |= VM_NORESERVE;
/* hugetlb applies strict overcommit unless MAP_NORESERVE */
if (file && is_file_hugepages(file))
vm_flags |= VM_NORESERVE;
}The first if is the mechanism behind the documented “In mode 2 the MAP_NORESERVE flag is ignored” — and note how it is ignored. The kernel does not check the mode at accounting time; it simply declines to set VM_NORESERVE on the VMA in the first place when the policy is OVERCOMMIT_NEVER. The flag is dropped at mmap entry, so the mapping is indistinguishable from one that never asked.
The second if runs unconditionally with respect to the mode, and it inverts the meaning of the flag for huge pages. For normal memory the kernel reserves nothing by default and MAP_NORESERVE is a no-op-ish hint; for hugetlb the comment says the opposite — “hugetlb applies strict overcommit unless MAP_NORESERVE.” Huge pages are reserved eagerly at mmap time out of the huge-page pool, and MAP_NORESERVE is how you opt out of that reservation, accepting a possible SIGBUS at fault time if the pool is empty. So the same flag means “don’t bother reserving, I probably won’t touch it” for normal memory and “skip the reservation I would otherwise be guaranteed” for hugetlb — a real semantic difference, not just a different code path.
This is also why huge pages appear twice in the overcommit machinery in seemingly contradictory ways. accountable_mapping() excludes them from Committed_AS because “hugetlb has its own accounting separate from the core VM,” and vm_commit_limit() subtracts hugetlb_total_pages() from totalram_pages() before applying overcommit_ratio. Both are the same decision expressed twice: huge pages are carved out of the normal pool and tracked by hugetlbfs’s own reservation counters, so they must neither be charged to Committed_AS nor counted as RAM available to back it. Double-counting in either direction would be a bug.
Demonstrated: Watching Committed_AS Move
All of the above is checkable from userspace without root, because Committed_AS is a global counter that any process can read while it allocates. The program below mmaps a series of regions, reading /proc/meminfo before and after each one, and prints how the counter moved. The machine is in the stock default mode 0, MemTotal 125 GiB, SwapTotal 8 GiB, so RAM + swap ≈ 133 GiB — the threshold mode 0’s single check compares against.
#define GiB (1024UL*1024*1024)
static long committed(void) { /* scrape Committed_AS from /proc/meminfo */
FILE *f = fopen("/proc/meminfo", "r"); char k[64]; long v; char u[16];
while (fscanf(f, "%63s %ld %15s", k, &v, u) >= 2)
if (!strcmp(k, "Committed_AS:")) { fclose(f); return v; }
fclose(f); return -1;
}
static void *try_map(const char *label, unsigned long len, int extra) {
long before = committed();
void *p = mmap(NULL, len, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|extra, -1, 0);
long after = committed();
printf("%-42s len=%4lu GiB -> %-14s Committed_AS %ld -> %ld kB (delta %+ld kB)\n",
label, len/GiB, p==MAP_FAILED ? strerror(errno) : "OK", before, after, after-before);
return p;
}Nothing here needs privileges — mmap of a huge MAP_NORESERVE region costs nothing, and reading /proc/meminfo is world-readable. Real output, unedited:
baseline Committed_AS = 84367360 kB
1. MAP_NORESERVE, private anon len= 40 GiB -> OK Committed_AS 84367360 -> 84367360 kB (delta +0 kB = +0 GiB)
2. accounted (no NORESERVE), private anon len= 40 GiB -> OK Committed_AS 84367360 -> 126310400 kB (delta +41943040 kB = +40 GiB)
3. accounted, further 8 GiB len= 8 GiB -> OK Committed_AS 126310400 -> 134699008 kB (delta +8388608 kB = +8 GiB)
4. accounted, 200 GiB > RAM+swap (133 GiB) len= 200 GiB -> Cannot allocate memory Committed_AS 134699008 -> 134699008 kB (delta +0 kB = +0 GiB)
5. NORESERVE, 200 GiB > RAM+swap len= 200 GiB -> OK Committed_AS 134699008 -> 134699008 kB (delta +0 kB = +0 GiB)
6. MAP_SHARED anon (shmem-backed) len= 40 GiB -> OK Committed_AS now 176642048 kB
-- touching 1 GiB of mapping 2 (faulting real pages) --
Committed_AS 176642048 -> 176642048 kB (delta +0 kB) <-- unchanged: commit != RSS
/proc/self/statm: VSZ 85983881 pages (328 GiB) RSS 262594 pages (1025 MiB)
-- unmapping mapping 2 --
Committed_AS 176642048 -> 134699008 kB (delta -41943040 kB)Every row confirms a specific claim made earlier in this note:
MAP_NORESERVEcosts exactly nothing. A 40 GiB mapping moved the counter by 0 kB.VM_NORESERVEwas set, soaccountable_mapping()returned false and__vm_enough_memory()was never called. The mapping is fully usable; it is simply invisible to the ledger.- An accounted mapping costs exactly its size. The same 40 GiB without
MAP_NORESERVEmoved the counter by 41,943,040 kB, which is 40 GiB to the byte. Not an estimate, not a fraction — the full length of the mapping, charged instantly atmmaptime, before a single page was touched. This is what “size of mapping per instance” means. - Charges accumulate. A further 8 GiB added exactly 8 GiB.
- Mode 0’s one real check, caught firing. A 200 GiB accounted request failed with
ENOMEM— 200 GiB exceeds the 133 GiB of RAM + swap, sopages > totalram_pages() + total_swap_pageswas true and__vm_enough_memory()took thegoto errorpath. This is mode 0 doing the only thing it does. Note what it did not care about:Committed_ASwas already 128 GiB andCommitLimitwas 70.5 GiB, both wildly exceeded, and neither entered the decision. - The same 200 GiB succeeds with
MAP_NORESERVE. Put rows 4 and 5 side by side: identical size, identical protection, identical flags but one bit — one fails, one succeeds. This is the cleanest possible demonstration thatMAP_NORESERVEdoes not merely skip a charge, it skips the check. There is no size ofMAP_NORESERVEmapping that mode 0 will refuse on accounting grounds (you will hitmax_map_countor run out of address space first). MAP_SHAREDanonymous is charged in full. 40 GiB of shared anonymous memory added 40 GiB, viashmem_acct_size()rather thanaccountable_mapping()— confirming the table row that is most often assumed to go the other way, since “shared” file mappings are free.- Faulting pages in does not move
Committed_AS.memset-ing 1 GiB of mapping 2 — genuinely dirtying 262,144 real pages — left the counter bit-for-bit identical while RSS went to 1,025 MiB. The two ledgers are completely decoupled. This one line is the whole section in miniature. munmaprefunds exactly. Releasing mapping 2 returned exactly-41,943,040 kB.
Also note the final state: Committed_AS reached 176,642,048 kB ≈ 168 GiB on a machine with 125 GiB of RAM and a CommitLimit of 70.5 GiB. That is 2.4× the CommitLimit and 1.35× all the RAM and swap in the box, reached by one unprivileged process in a few milliseconds, with no error and no memory pressure whatsoever — RSS at that moment was about 1 GiB. And VSZ of 328 GiB exceeds even Committed_AS, because VSZ counts the MAP_NORESERVE regions too.
sequenceDiagram autonumber participant P as Process participant K as Kernel (mm/util.c) participant L as Committed_AS ledger participant F as Physical pages (RSS) Note over L,F: baseline: Committed_AS 80.6 GiB, RSS ~0 P->>K: mmap(40 GiB, MAP_NORESERVE) K-->>P: OK (accountable_mapping() = false,<br/>check skipped entirely) Note over L: unchanged - 80.6 GiB P->>K: mmap(40 GiB, accounted) K->>K: __vm_enough_memory(pages=40 GiB) K->>L: vm_acct_memory(+40 GiB) K-->>P: OK (mode 0: 40 GiB < RAM+swap) Note over L: 120.6 GiB Note over F: still ~0 - nothing touched P->>K: mmap(200 GiB, accounted) K->>K: pages > totalram + total_swap ? K->>L: vm_unacct_memory(-200 GiB) (roll back) K-->>P: -ENOMEM Note over L: unchanged P->>F: memset(buf, 1, 1 GiB) F->>F: 262144 page faults, real pages allocated Note over L: STILL unchanged Note over F: RSS 1025 MiB P->>K: munmap(40 GiB) K->>L: vm_unacct_memory(-40 GiB) Note over L: back to 80.6 GiB Note over F: RSS drops too - but for a different reason
The measured session as a sequence, showing which ledger each step touches. What it shows: the ledger and the physical pages are updated by different events. Steps 4-5 touch the ledger with no physical memory involved; step 11 allocates real physical memory with no ledger movement at all. The rollback in step 9 is __vm_enough_memory()’s error path — it charges first via vm_acct_memory(), then un-charges on refusal, which is why a failed allocation leaves no trace. The insight to take: if you are trying to answer “how much memory is this system using,” every arrow in this diagram that touches Committed_AS is noise, and the single arrow that touches RSS is your answer.
Uncertain
Verify: the behavior of all three modes on this machine. Reason: only mode 0 was measured. There is no passwordless
sudoon this host, sovm.overcommit_memorycould not be changed — the mode-1 and mode-2 rows of the tables above rest on the v6.12 source and the kernel documentation, not on local measurement. To resolve: runsudo sysctl -w vm.overcommit_memory=2and rerun the same program; the expected result is that mapping 1 now also charges 40 GiB (becauseVM_NORESERVEis not set in mode 2) and that every mapping fails immediately, since this machine’sCommitted_ASalready exceedsCommitLimitby 10 GiB before the program starts. Restore withsudo sysctl -w vm.overcommit_memory=0. uncertain
The Relationship to the OOM Killer
Overcommit and the OOM killer are two ends of the same policy. The connection is causal:
- Under modes 0 and 1, the kernel deliberately promises more than it can back. As long as processes don’t touch all their promises, physical RAM suffices. But when collective resident usage (the pages actually faulted in via demand paging) outgrows RAM + swap, the kernel cannot satisfy a fault and cannot fail it — the page fault is not a system call that can return
-ENOMEMto the program. Its only recourse is to free memory by killing a process, which is precisely what the OOM killer does, selecting a victim via the [[OOM Score and Victim Selection|oom_badness()heuristic]]. Overcommit is why the OOM killer must exist: lazy commitment defers the accounting failure to fault time, where the only lever left is termination. - Under mode 2, the kernel refuses to overcommit, so the failure surfaces early — at the
mmap/brkthat would pushCommitted_ASoverCommitLimit, returning-ENOMEM. A program that checks its allocation return values can degrade gracefully. The OOM killer should rarely or never fire (barring kernel allocations and edge cases). Mode 2 trades the OOM killer’s surprise for up-front allocation failures the application must be written to handle.
oom_score vs oom_score_adj — Two Different Numbers
Because overcommit is what makes the OOM killer necessary, the two knobs that steer it belong in this note even though The OOM Killer and OOM Score and Victim Selection own the victim-selection mechanism in depth. The pair is constantly conflated, so: they are not the same number, not even the same units, and only one of them is writable.
/proc/<pid>/oom_score | /proc/<pid>/oom_score_adj | |
|---|---|---|
| Direction | read-only | read/write — this is the knob |
| Range | 0 – 2000 (an exported scale) | −1000 to +1000 (OOM_SCORE_ADJ_MIN … OOM_SCORE_ADJ_MAX) |
| Units | a normalized score | permille of totalpages — the value is multiplied by totalpages / 1000 before being added |
| Meaning | the current badness of this task, computed on read | your bias: negative protects, positive volunteers |
| Set by | the kernel | the admin, systemd’s OOMScoreAdjust=, or the process itself |
| Special value | 0 also means “unkillable” | −1000 disables OOM-killing for the task entirely |
The underlying oom_badness() (mm/oom_kill.c:202-240, v6.12) makes the units explicit:
/*
* The baseline for the badness score is the proportion of RAM that each
* task's rss, pagetable and swap space use.
*/
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
mm_pgtables_bytes(p->mm) / PAGE_SIZE;
task_unlock(p);
/* Normalize to oom_score_adj units */
adj *= totalpages / 1000;
points += adj;This is the crucial connection back to overcommit: the baseline is rss + swap-entries + page-table pages. Not Committed_AS, not total_vm, not anything the overcommit ledger tracks. The OOM killer scores processes on the memory they actually occupy. So the two halves of this note’s subject use opposite measures — admission control counts promises, victim selection counts pages. A process that reserved 200 GiB and touched 4 MiB is enormous to Committed_AS and invisible to the OOM killer; a process that reserved 4 MiB with MAP_NORESERVE and touched all of it is the reverse.
The adj *= totalpages / 1000 line is why oom_score_adj is a permille knob: on this 125 GiB + 8 GiB machine totalpages is 34,884,705 pages, so each point of oom_score_adj is worth 34,884 pages ≈ 136 MiB of pretend-RSS. An oom_score_adj of +300 adds ~10.5 million pages — about 40 GiB of imaginary memory — which is why positive adjustments dominate the real RSS of most processes.
The exported oom_score is then a rescaling done at read time in proc_oom_score() (fs/proc/base.c:586-602, v6.12):
badness = oom_badness(task, totalpages);
/*
* Special case OOM_SCORE_ADJ_MIN for all others scale the
* badness value into [0, 2000] range which we have been
* exporting for a long time so userspace might depend on it.
*/
if (badness != LONG_MIN)
points = (1000 + badness * 1000 / (long)totalpages) * 2 / 3;Checked against live processes on the measurement machine:
$ pid=9557; cat /proc/$pid/comm /proc/$pid/oom_score /proc/$pid/oom_score_adj
brave
870
300
$ cat /proc/$pid/statm # fields: size resident shared text lib data dt
380808470 229725 52611 60180 0 644045 0
$ for p in 1 3838 3879; do echo "$(cat /proc/$p/comm) score=$(cat /proc/$p/oom_score) adj=$(cat /proc/$p/oom_score_adj)"; done
systemd score=0 adj=0
systemd-journal score=500 adj=-250
systemd-udevd score=0 adj=-1000Substituting the brave numbers into the formula: badness ≈ 229,725 (resident pages) + 300 × 34,884 (the adj contribution) = 10,694,925; then (1000 + 10,694,925 × 1000 / 34,884,705) × 2 / 3 = (1000 + 306) × 2 / 3 = 870 — exactly the reported oom_score. Note this browser has volunteered itself with adj = +300, which is normal for Chromium-family renderer processes: they are the most expendable thing on a desktop, and the parent sets them up to be killed first.
The two zeros in the last block are different zeros, and the distinction is worth seeing. systemd-udevd reports 0 because adj == OOM_SCORE_ADJ_MIN makes oom_badness() return LONG_MIN, which proc_oom_score() special-cases to 0 — it is exempt. systemd (PID 1) reports 0 with adj = 0, because oom_badness() begins with if (oom_unkillable_task(p)) return LONG_MIN; and the global init is unconditionally unkillable — no oom_score_adj setting was needed or would help. A reported oom_score of 0 therefore does not distinguish “tiny” from “immune.” Read oom_score_adj to tell which.
This is the central trade-off: modes 0/1 optimize memory utilization and accept tail OOM-kill risk; mode 2 sacrifices utilization for predictable, handleable allocation failures. Databases and latency-critical services that cannot tolerate a kill sometimes run mode 2 with generous CommitLimit; general-purpose systems run the default mode 0.
Configuration Examples
# Inspect current policy and accounting
sysctl vm.overcommit_memory # 0, 1, or 2
grep -E 'CommitLimit|Committed_AS' /proc/meminfo
# Mode 1: always overcommit (e.g. for a Redis box that forks for RDB snapshots)
sysctl -w vm.overcommit_memory=1 # avoids fork() failing when the child is short-lived
# Mode 2: strict, ceiling = swap + 80% of RAM
sysctl -w vm.overcommit_memory=2
sysctl -w vm.overcommit_ratio=80 # percentage form
# OR an absolute ceiling instead of a percentage:
sysctl -w vm.overcommit_kbytes=8388608 # 8 GiB of RAM (+ swap); zeroes overcommit_ratioThe Redis case (line 8) is a classic real-world mode-1 use. Redis fork()s to write a background snapshot, and the child shares the parent’s heap via COW, touching almost none of the inherited pages. Redis’s documentation explicitly instructs operators to “Set the Linux kernel overcommit memory setting to 1. Add vm.overcommit_memory = 1 to /etc/sysctl.conf” precisely to avoid “Background saving fails with a fork() error on Linux” (Redis administration docs). Note the mechanism: fork() does run the overcommit check — dup_mmap() in kernel/fork.c calls security_vm_enough_memory_mm() (→ __vm_enough_memory()) for each accountable (VM_ACCOUNT) VMA, charging the mapping’s length (kernel/fork.c, v6.12). Under the default mode 0, that charge is rejected only if a single VMA’s length exceeds RAM+swap, so a normal Redis instance’s fork is not blocked by mode 0; the documented fork-failure regime is mode 2 (strict accounting), where charging the child’s full private-writable footprint can push Committed_AS over CommitLimit. Setting mode 1 sidesteps the check entirely and is the simplest fix regardless of which mode caused the failure.
Uncertain
Verify: the attribution of Redis’s documented
fork()failure to mode 2 specifically. Reason: the reasoning above is an inference from__vm_enough_memory()’s mode-0 branch (a single VMA rarely exceeds RAM+swap, so mode 0 should not block a normal fork) rather than something Redis’s documentation states — Redis says only to set the sysctl to 1, without naming the mode that was failing. A competing explanation exists: on a machine with little or no swap and a very large Redis dataset, a single private-writable VMA genuinely can exceedtotalram_pages() + total_swap_pages, in which case mode 0 would refuse the fork. Both mechanisms are real; which one operators actually hit is not established here. To resolve: reproduce on a host withSwapTotal: 0, a Redis instance holding more thanMemTotalworth of a single mapping, andvm.overcommit_memory=0, and check whetherBGSAVEfails; then repeat under mode 2. uncertain
Failure Modes and Common Misunderstandings
- “
CommitLimitis enforced.” Only in mode 2. The default mode 0 displays it but ignores it. Many a confused operator has seenCommitted_ASexceedCommitLimitand assumed a bug. - “
Committed_ASis how much RAM is in use.” No — it is how much address space has been promised, including untouched COW pages, sparse mappings, and reserved-but-unfaulted anonymous memory. Actual resident usage isMemTotal - MemAvailable(roughly).Committed_ASroutinely far exceeds real usage under overcommit, and that gap is the point. MAP_NORESERVEis ignored in mode 2. Normallymmap(..., MAP_NORESERVE)asks the kernel to skip overcommit accounting for that mapping; the doc notes that in mode 2 this flag is ignored (overcommit-accounting.rst), so even “no-reserve” mappings count against the limit.- Stack growth corner case. The doc warns that C stack growth does an implicit
mremap, so a program running near the mode-2 ceiling that wants absolute guarantees “MUSTmmapyour stack for the largest size you think you will need” — otherwise a deep call chain can hit-ENOMEMmid-execution (overcommit-accounting.rst). - The accounting doc’s “To Do: implement actual limit enforcement.” The
overcommit-accounting.rst“Status”/“To Do” lists are historical and partly stale (they predate the modern enforcement in__vm_enough_memory); read the rules sections as current and the status checklist as legacy context.
Alternatives and When to Choose Them
- Default (mode 0) for nearly all general-purpose systems: maximum memory efficiency, OOM killer as the safety net. Pair with sensible [[OOM Score and Victim Selection|
oom_score_adj]] values to protect critical services. - Mode 1 for workloads that legitimately reserve vast sparse/COW memory they won’t touch (scientific sparse arrays, fork-heavy services like Redis snapshotting). Accepts that the OOM killer is the only backstop.
- Mode 2 for systems that must never be surprised by a kill and whose applications check allocation returns — at the cost of stranding RAM you’ve reserved against the limit. Tune
overcommit_ratio/overcommit_kbyteswith swap in mind, since both addtotal_swap_pages. - Per-cgroup limits (memcg
memory.max/memory.high) are a different and complementary mechanism: they bound resident usage per group and trigger per-cgroup reclaim and OOM, rather than bounding system-wide committed address space. Containerized systems usually rely on memcg, not global mode 2.
See Also
- The OOM Killer — the consequence of modes 0/1: when committed promises come due faster than RAM can satisfy them, the OOM killer terminates a process to recover memory.
- OOM Score and Victim Selection — which process the OOM killer picks; its
oom_badness()score reflects resident reality, the flip side of the committed promises this note accounts for. - Copy-on-Write and fork — the canonical reason overcommit pays off:
fork()duplicates an address space cheaply because shared pages are charged at “size per instance” only when written. - Demand Paging — the mechanism that defers physical backing to first touch, making lazy commitment possible.
- Memory Reclaim Overview — the kernel’s first response to pressure, tried before the OOM killer; mode 2 aims to avoid reaching either.
- hugetlbfs and Reserved Huge Pages — huge pages are excluded from
Committed_ASand subtracted from the RAM feedingCommitLimit; they have their own eager reservation system, which is whyMAP_NORESERVEinverts meaning for them. - Anonymous vs File-Backed Memory — the distinction that decides whether a mapping is charged at all: a page with a file behind it needs no commitment, an anonymous one does.
- brk and mmap as Allocator Backends — the two syscalls that do most of the charging, and how
glibc’s allocator drives them. - Virtual Memory Areas —
VM_ACCOUNTandVM_NORESERVEare VMA flags; the accounting is per-VMA bookkeeping. - The Memory Cgroup memcg — the per-container mechanism that bounds resident usage, the complement to this note’s system-wide committed accounting.
- MOC: Linux Memory Management MOC (§1, Virtual Memory and the Process Address Space; cross-cuts §11, the OOM killer).