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, a fork() that duplicates a multi-gigabyte address space via copy-on-write, a sparse array mapped with mmap. 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, the Committed_AS and CommitLimit fields 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 &gt;<br/>RAM + swap?"}
  GUESS -->|no| OK0["Succeed"]
  GUESS -->|"yes (wild alloc)"| FAIL0["-ENOMEM"]
  MODE -->|"2 = NEVER"| LIMIT{"Committed_AS + req<br/>&gt; 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:

  • malloc over-allocation. glibc’s allocator grows the heap with brk(2)/sbrk(2) and serves allocations larger than MMAP_THRESHOLD (128 KB by default) with a private anonymous mmap(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 immediately execs a tiny program would, under eager accounting, momentarily “owe” 8 GiB it never uses. The overcommit-accounting doc captures this rule precisely: a PRIVATE WRITABLE anonymous 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/>&gt; RAM+swap? no"| PASS["charge accepted"]
  CHECK -->|"mode 2: would<br/>Committed_AS + 4 GiB<br/>&gt; 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) returns 0 (success) unconditionally after charging the accounting counter via vm_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” and vm.rst’s “rejects obvious overcommits” (vm.rst, v6.12): mode 0 catches only the catastrophically-wild single allocation (a corrupted size, an accidental 1 << 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 computes allowed = vm_commit_limit() (the CommitLimit, 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 exceed allowed. On refusal it logs a rate-limited warning, un-accounts the pages, and returns -ENOMEM — a clean error the calling program sees as a failed mmap/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 at mm->total_vm / 32, keeps a single runaway process from consuming so much that the user cannot even run kill to 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_memoryConstantAdmission rule actually evaluatedIs a cumulative total consulted?Where it can fail
0 (default)OVERCOMMIT_GUESSpages > totalram_pages() + total_swap_pages ⇒ -ENOMEM; otherwise succeedNo — only this one request’s sizefault time (OOM killer), plus the single-request sanity check
1OVERCOMMIT_ALWAYSnone — return 0 immediately after chargingNofault time only (OOM killer)
2OVERCOMMIT_NEVERvm_committed_as + pages > vm_commit_limit() − admin_reserve − min(mm->total_vm/32, user_reserve) ⇒ -ENOMEMYes — vm_committed_as, the system-wide running totalallocation 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 = 131072

8,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 default CommitLimit is 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/>&gt;&gt; (PAGE_SHIFT - 10)<br/>i.e. kBytes / 4 = pages"]
  KB -->|"no — percentage mode<br/>(the default)"| PCT["allowed = (totalram_pages()<br/>&minus; hugetlb_total_pages())<br/>&times; 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 &times; 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);
  • CommitLimit is exactly vm_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 a CommitLimit does not mean the system is enforcing it. In the default mode 0, CommitLimit is purely informational. It is the threshold that would apply if you switched to mode 2.
  • Committed_AS (“committed address space”) is the running total vm_committed_as — the sum of all memory the kernel has promised to back, across every process, computed by the accounting rules in overcommit-accounting.rst (private-writable anonymous = full size, shared/read-only file = 0, and so on). When Committed_AS exceeds CommitLimit, 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:

FieldWhat it isExact or estimate?
MemTotalusable RAM (total physical minus kernel reserved/firmware)exact, fixed at boot (changes only on memory hotplug)
SwapTotaltotal swapexact
CommitLimitvm_commit_limit() — swap + ratio% × (RAM − hugetlb)exact, a pure function of three sysctls plus MemTotal. Recomputed on every read
Committed_ASvm_memory_committed() — the promise ledgereffectively exact when read here (see below), but not the value the admission check uses
MemFreepages on the free lists right nowexact, but nearly useless in isolation — a healthy Linux box keeps MemFree low on purpose, using the rest as page cache
MemAvailablesi_mem_available() — how much could be handed out without swappingan 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 / Hugetlbhuge-page poolexact — and note Hugetlb is subtracted from the RAM that feeds CommitLimit
VmallocTotalsize of the kernel’s vmalloc address rangeexact 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/meminfo uses the accurate read. vm_memory_committed() calls percpu_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 where vm_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() calls percpu_counter_read_positive(), which reads only the folded global total and ignores the unfolded per-CPU deltas. It must — it runs on every mmap, 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:

ModeFormulaBatch per CPUWorst-case total skew (batch × nr_cpus)
0 or 1 (GUESS/ALWAYS)ram_pages / 32 / 4256,152 pages ≈ 1,000 MiB≈ 31.3 GiB
2 (NEVER)ram_pages / 32 / 2564,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 kB

Read those four numbers against each other:

  • CommitLimit is 73,963,712 kB ≈ 70.5 GiB. Check the arithmetic: overcommit_ratio is 50 and overcommit_kbytes is 0, so the formula is 0.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 that hugetlb_total_pages() is zero here (HugePages_Total: 0 in /proc/meminfo).
  • Committed_AS is 84,508,764 kB ≈ 80.6 GiB, which is 114% of CommitLimit — already over the ceiling, at rest, with no unusual workload.
  • And yet MemAvailable is 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 &gt; RAM+swap is NORMAL,<br/>and Committed_AS &gt; 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:

CallerFile and lineWhat it charges
__mmap_region()mm/mmap.c:1410a new private-writable mapping, gated on accountable_mapping()
do_brk_flags()mm/mmap.c:1794heap growth via brk; the VMA is created with VM_ACCOUNT unconditionally
acct_stack_growth()mm/mmap.c:1032one stack-expansion step — the “implicit mremap” the kernel doc’s Gotchas section warns about
insert_vm_struct()mm/mmap.c:1975a VMA inserted outside the normal mmap path, if it carries VM_ACCOUNT
mprotect_fixup()mm/mprotect.c:655making an existing read-only private mapping writable — the moment a 0-cost mapping becomes a full-cost one
move_vma()mm/mremap.c:708a relocating mremap
SYSCALL_DEFINE5(mremap, ...)mm/mremap.c:1126an in-place mremap that grows
dup_mmap()kernel/fork.c:688each 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, 207tmpfs/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:

MappingAccounted against Committed_AS?Why
MAP_PRIVATE anonymous, writableYes — full sizethe canonical case: exactly VM_WRITE. “size of mapping per instance”
MAP_PRIVATE anonymous, PROT_READ onlyNoVM_WRITE clear. Doc: “PRIVATE READ-only — 0 cost (but of little use)”
MAP_PRIVATE file-backed, writableYes — full sizeprivate writes go to swap, not the file, so they need backing
MAP_PRIVATE file-backed, read-onlyNothe file is the backing store
MAP_SHARED file-backed (any protection)NoVM_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 sizenot 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)NoVM_NORESERVE set ⇒ __vm_enough_memory() is never even called
Anything with MAP_NORESERVE (mode 2)YesVM_NORESERVE is not set in mode 2 — see below
MAP_DROPPABLE (since v6.11)Nommap.c sets VM_NORESERVE for it: “If the pages can be dropped, then it doesn’t make sense to reserve them”
hugetlbfs / MAP_HUGETLBNo — separate system entirelyaccountable_mapping() returns false outright for a hugepage file
mprotect read-only → writable on a private mappingYes, charged at mprotect timemprotect_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:

  1. MAP_NORESERVE costs exactly nothing. A 40 GiB mapping moved the counter by 0 kB. VM_NORESERVE was set, so accountable_mapping() returned false and __vm_enough_memory() was never called. The mapping is fully usable; it is simply invisible to the ledger.
  2. An accounted mapping costs exactly its size. The same 40 GiB without MAP_NORESERVE moved 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 at mmap time, before a single page was touched. This is what “size of mapping per instance” means.
  3. Charges accumulate. A further 8 GiB added exactly 8 GiB.
  4. 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, so pages > totalram_pages() + total_swap_pages was true and __vm_enough_memory() took the goto error path. This is mode 0 doing the only thing it does. Note what it did not care about: Committed_AS was already 128 GiB and CommitLimit was 70.5 GiB, both wildly exceeded, and neither entered the decision.
  5. 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 that MAP_NORESERVE does not merely skip a charge, it skips the check. There is no size of MAP_NORESERVE mapping that mode 0 will refuse on accounting grounds (you will hit max_map_count or run out of address space first).
  6. MAP_SHARED anonymous is charged in full. 40 GiB of shared anonymous memory added 40 GiB, via shmem_acct_size() rather than accountable_mapping() — confirming the table row that is most often assumed to go the other way, since “shared” file mappings are free.
  7. 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.
  8. munmap refunds 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 sudo on this host, so vm.overcommit_memory could 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: run sudo sysctl -w vm.overcommit_memory=2 and rerun the same program; the expected result is that mapping 1 now also charges 40 GiB (because VM_NORESERVE is not set in mode 2) and that every mapping fails immediately, since this machine’s Committed_AS already exceeds CommitLimit by 10 GiB before the program starts. Restore with sudo 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 -ENOMEM to 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/brk that would push Committed_AS over CommitLimit, 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
Directionread-onlyread/write — this is the knob
Range0 – 2000 (an exported scale)−1000 to +1000 (OOM_SCORE_ADJ_MIN … OOM_SCORE_ADJ_MAX)
Unitsa normalized scorepermille of totalpages — the value is multiplied by totalpages / 1000 before being added
Meaningthe current badness of this task, computed on readyour bias: negative protects, positive volunteers
Set bythe kernelthe admin, systemd’s OOMScoreAdjust=, or the process itself
Special value0 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=-1000

Substituting 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_ratio

The 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 exceed totalram_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 with SwapTotal: 0, a Redis instance holding more than MemTotal worth of a single mapping, and vm.overcommit_memory=0, and check whether BGSAVE fails; then repeat under mode 2. uncertain

Failure Modes and Common Misunderstandings

  • “CommitLimit is enforced.” Only in mode 2. The default mode 0 displays it but ignores it. Many a confused operator has seen Committed_AS exceed CommitLimit and assumed a bug.
  • “Committed_AS is 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 is MemTotal - MemAvailable (roughly). Committed_AS routinely far exceeds real usage under overcommit, and that gap is the point.
  • MAP_NORESERVE is ignored in mode 2. Normally mmap(..., 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 “MUST mmap your stack for the largest size you think you will need” — otherwise a deep call chain can hit -ENOMEM mid-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_kbytes with swap in mind, since both add total_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_AS and subtracted from the RAM feeding CommitLimit; they have their own eager reservation system, which is why MAP_NORESERVE inverts 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_ACCOUNT and VM_NORESERVE are 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).