What mglru actually replaces
The multi-generational LRU (mglru, also lru_gen in the kernel source) is the replacement page-reclaim algorithm merged in Linux 6.1 (commit c174c2cd0441, end of 2022, Yu Zhao’s 12-patch series). It sits next to the classic active/inactive LRU — both compile paths exist in mm/vmscan.c and you choose at boot with CONFIG_LRU_GEN_ENABLED.
The premise: the classic LRU maintains two lists per zone (active and inactive) and tries to figure out which pages are cold by sampling and moving pages across the boundary. That algorithm has well-known pain points — false negatives when inactive lists get polluted, expensive rescaling, hot/cold confusion on big one-shot reads. mglru throws out the two-list model and replaces it with a time-bucketed generational array plus a page-table-walk hotness detector.
Both implementations share the page cache and the swap-out path; only the reclaim policy differs. So enabling mglru at runtime is safe — you’re not switching memory accounting models, just the eviction heuristic.
As of early 2026 mglru has shipped in mainline for over three years. But: the surrounding conversation has shifted. At the 2026 LSFMM+BPF Summit, Matthew Wilcox publicly said the algorithm’s maintainer has “moved on to other projects,” and called for ripping the whole thing out of mainline. The argument is that patches are accepted without review from people on the MAINTAINERS list, that no commits land in mm/ from those three maintainers, and that downstream vendors (Honor, Android) have accumulated out-of-tree patches to make mglru behave the way they need. That tension is the reason this article spends as much time on “what’s working” and “what’s not” as on the algorithm itself.
The generational model — pages bucketed by recency

mglru arranges anonymous and file-backed pages into four generations (compile-time constant MAX_NR_GENS). Each generation is a time window. The newest generation (highest gen) holds pages that have been touched recently; older generations hold pages that have gone stale.
Gen N : pages accessed within the last few seconds
Gen N-1 : pages accessed 10s of seconds ago
Gen N-2 : pages idle for ~minutes
Gen N-3 : oldest, "eviction candidates" — what gets reclaimed firstWhen the kernel needs memory, it walks from the oldest non-empty generation toward newer ones, dropping clean pages and writing out dirty ones. Promotion (a page touched in gen N-1, moves to gen N) happens via two access-detectors:
- Page-table walk — the kernel walks the page tables of a few processes, looking at PTE.Accessed bits. If set, promote.
- rmap walk on isolated pages — for pages already on the LRU being scanned, look at the rmap for any PTEs referencing them that have set Accessed.
Promotion does not move a page physically between lists. Instead it stores the generation in the page’s folio->flags bits (more on this in a moment) and updates the per-generation counter (nr_pages[gen][type][zone]) to reflect the new bucket.
The per-node hotness state lives in struct lru_gen_folio:
/* mm/vmscan.c */
struct lru_gen_folio {
unsigned long max_seq; // newest gen number
unsigned long min_seq[ANON_AND_FILE]; // oldest non-empty gen per type
long nr_pages[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES];
unsigned long *filters[NR_BLOOM_FILTERS]; // bloom for refault detection
atomic_long_t nr_evicted[ANON_AND_FILE];
atomic_long_t nr_refaulted[ANON_AND_FILE];
};max_seq is bumped by lru_gen_rotate() whenever the kernel asks “what’s new?” (every kswapd cycle, every try_to_free_pages). The four generations are tracked relative to max_seq, so old generations drift to “max_seq − 1”, “max_seq − 2”, etc. When max_seq − min_seq > MAX_NR_GENS the oldest is dropped and its pages evicted.
The page-table-walk trick — and why x86 silicon matters
mglru’s most-cited win is that it can find recently-touched pages without scanning the rmap. Instead, on every aging cycle, it does a controlled page-table walk over a sample of processes’ address spaces. The walk iterates PTE entries, looks at Accessed (architecture bit; on x86 it’s bit 5), and promotes pages whose bit is set.
But there’s a problem: the walker writes the Accessed bit back to 0 after reading it. On x86 that’s done by hardware via CLFLUSH/CLFLUSHOPT for non-leaf entries or specific instruction sequences. The kernel’s choice to clear in large batches is what the kill-switch flags 0x0002 (leaf) and 0x0004 (non-leaf) control:
| Value | Effect | Trade-off |
|---|---|---|
0x0001 |
Master switch | turn on/off the whole algorithm |
0x0002 |
Batch-clear leaf PTE Accessed bits | optimizes TLB work, may increase mmap_lock contention |
0x0004 |
Batch-clear non-leaf PTE Accessed bits | works on Intel/AMD x86 specifically; unclear elsewhere |
Most production turn-on is 0x0007 (all three). Disabling batch-clearing gives back some atomicity on the walk but loses the optimization that made mglru faster than active/inactive in the first place.
The kernel calls this in walk_pte_range() → lru_gen_pte_entry() for each PTE. The walk target is a per-memcg priority list (memcg_lru_gen_prio); see the docs at Documentation/admin-guide/mm/multigen_lru.rst for the exact heuristic.
Anonymous vs. file — the balance problem
This is the single most commonly reported mglru problem, repeated in the 2026 LSFMM discussions by Honor/Android engineers.
Classic LRU’s swappiness sysctl directly biases how aggressive the kernel should be at swapping out anonymous pages vs. file cache. mglru tries to handle the same bias internally — but the empirical finding is:
Anonymous pages tend to cluster in the youngest two generations (max_seq and max_seq-1). File-backed pages drift to older generations. End result: anon pages are essentially never reclaimed, and file pages are reclaimed aggressively.
swappiness doesn’t fully fix this. Honor’s workaround in production Android kernel forks is to manipulate memory cgroups’ memory.high to artificially force anon reclaim on background apps. That’s an out-of-tree hack.
In mainline this is unresolved as of 6.10. If your workload runs anonymous-heavy (e.g. JVM workloads pinned in RAM, large Redis-style stores, in-memory databases), mglru can leak memory unless you tune at the cgroup level.
The page-flag budget — three flags out of a scarce pool
mglru uses three bits in folio->flags to track page generation:
LRU_GEN_BIT = PG_reclaim(one of the reclaim flag bits that classic LRU was already using)LRU_USAGE_BIT = PG_young- a third bit shared with classic LRU’s workingset tracking
Flags in folio->flags are a hard-currency resource in the kernel. Every subsystem wants more. The objection raised at LSFMM 2026 was that this design cannot scale: you’d want more generations (e.g. to track longer history), but each new generation needs at least one more flag. Kairui Song from SUSE proposed moving the gen tracking into the folio’s rmap metadata, freeing up three flags and unlocking 63 generations. That patch series hasn’t merged as of early 2026.
How it interacts with memory cgroups
If you read our companion piece on Linux memory cgroup v2: how the kernel enforces the budget, you remember that each memcg has its own LRU. mglru keeps that model. Each memcg gets its own lru_gen_folio and its own max_seq. The cgroup v2 reclaim path walks the memcg’s LRU through mglru’s lru_gen_scan() function, which honors per-cgroup pressure signals.
But here’s the interaction that bites people in production: mglru tends to over-reclaim from control groups. When global memory pressure triggers a node-wide scan, the algorithm’s “just walk from oldest gen” logic can drop clean pages from a memcg that wasn’t actually at its budget. Symptom: a memcg at memory.current < memory.high losing pages to reclaim, triggering thrash inside the workload.
The fix path discussed at LSFMM involves better coordination between memcg_vmpressure and mglru’s per-memcg min_seq. Not merged in mainline as of 6.10.
Practical workaround: pair mglru with explicit memory.high on production workloads so the cgroup’s soft limit acts as a guard rail. Set memory.high = memory.max * 0.8 and let the kernel throttle at high rather than letting global reclaim sweep through.
How it interacts with PSI
Linux PSI memory pressure: what it really measures covered the accounting model. mglru does fire PSI memstall events correctly — psi_memstall_enter is called from the same paths whether the underlying reclaim algorithm is active/inactive or mglru.
The interesting interaction is min_ttl_ms:
# Default: thrashing prevention off
echo 0 > /sys/kernel/mm/lru_gen/min_ttl_ms
# Protect last 1s of working set from eviction
echo 1000 > /sys/kernel/mm/lru_gen/min_ttl_msmin_ttl_ms instructs mglru: “don’t evict pages accessed within the last N ms.” When the kernel needs memory more urgently than the working-set boundary, the OOM killer fires. The whole feature targets laptop/desktop workloads without oomd, where jank feels worse than an OOM kill.
Production rule of thumb: min_ttl_ms=1000 works for most interactive workloads; 3000 if you tolerate aggressive-killing. On a server where there’s no human, don’t enable it — you want the kernel to take memory from cold anon pages without waiting.
Runtime knobs (the entire stable ABI)

Everything stable lives under /sys/kernel/mm/lru_gen/:
ls /sys/kernel/mm/lru_gen/
# enabled min_ttl_ms# Check whether mglru is on (the kernel still keeps active/inactive code compiled
# in case the user turns it off)
cat /sys/kernel/mm/lru_gen/enabled
# 0x0007 (mglru on, leaf-clearing on, non-leaf-clearing on)
# Disable cleanly — falls back to classic active/inactive
echo 0x0000 > /sys/kernel/mm/lru_gen/enabled
# Toggle components granularly
echo 0x0001 > /sys/kernel/mm/lru_gen/enabled # mglru without batch-clearing
echo 0x0005 > /sys/kernel/mm/lru_gen/enabled # mglru + non-leaf only (no leaf-clearing)Note that 0x0005 is interesting — it disables leaf-PTE clearing only, leaving the non-leaf clearing. On workloads with heavily-contended mmap_lock (lots of short-lived mmaps, like JVM forks or browser engines), turning off leaf-clearing can save real time because the leaf-clearing path acquires the mmap_lock reader in some scheduling paths.
Experimental features under debugfs
/sys/kernel/debug/lru_gen exposes advanced capabilities. The format is one command per line, multiple lines per write allowed, separated by , or ;. The two practical uses are working set estimation and proactive reclaim — both aimed at data-center job schedulers.
Working-set estimation
Reading without a command returns a histogram of pages accessed over different time windows per memcg and per node:
memcg 0 /
node 0
min_gen_nr 7546 982 4096
min_gen_nr 7545 652 3096
...
max_gen_nr 7548 128 256Each bin records pages accessed within a window (age_in_ms). The bins are noncumulative. MAX_NR_GENS decides the bin count.
Realistic use: a Kubernetes scheduler estimates cold page size per node. If server-A has a working-set size of 4 GiB for the next 1s, and the next job wants 8 GiB, that node’s a poor fit. Bin-packing can use this directly.
To populate the histogram with a fresh generation:
echo "+ <memcg_id> <node_id> <max_gen_nr+1>" > /sys/kernel/debug/lru_genProactive reclaim (cold pages only, no pressure needed)
echo "- <memcg_id> <node_id> <min_gen_nr>" > /sys/kernel/debug/lru_genThis evicts pages in generations ≤ min_gen_nr, without waiting for global pressure. Useful when a scheduler lands a new job and wants to free up cold pages ahead of it.
Constraint: min_gen_nr must be < max_gen_nr - 1, because max_gen_nr and max_gen_nr-1 are the “active list equivalent” and aren’t fully aged. Eviction of those would be a bug.
Optional parameters:
echo "- <memcg_id> <node_id> <min_gen_nr> [<swappiness>] [<nr_to_reclaim>]" \
> /sys/kernel/debug/lru_genswappiness: 0–200, 200 = anon-only reclaim.nr_to_reclaim: stop after this many pages.
When mglru helps — and when it hurts
There are real workloads where mglru measurably wins:
- ChromeOS / Android devices: Google has published multiple testimonials. A 5.15-era ChromeOS with 8 GiB and zswap showed measurable reduction in SWAP-storms. MongoDB on POWER10 gets +~19% throughput under YCSB-like benchmarks. fio shows +5–40% depending on access distribution.
- Database servers: key-value workloads (memcached, redis) with high random-access locality.
- Mixed workloads: when running many small workloads with disjoint working sets, mglru’s time-based recency tracking helps isolate them.
And there are real workloads where mglru measurably hurts:
- Anonymous-heavy jobs that expect swap-out to free RSS. mglru under-reclaims anon even with
swappiness=200. Honors’ workaround uses memcgs to force the issue. - cgroup-heavy containers: over-reclaim from control groups that aren’t at their budget.
- Low-memory devices: with little to reclaim, the page-table-walk cost amortizes poorly. Classic LRU’s “scan the inactive list” is cheaper when there’s nothing to find.
- Workloads with lots of readahead: readahead pages go to the youngest generation but aren’t necessarily used, displacing pages that will be used. The fix is in flight (touches the page-table-walk to skip readahead pages), but not merged.
The 2026 reconsideration debate

As of the LSFMM+BPF Summit in May 2026 (per LWN’s reporting from Jonathan Corbet, March 2026):
- Matthew Wilcox (Intel) publicly called for removal: “rip it out.” His case is primarily about maintenance quality, not the algorithm itself. After commit
44958000badaadded threeMAINTAINERSentries, none of those three people has any commits inmm/in the six months since. Yu Zhao, the original author, has moved on. - Yu Zhao made a handful of commits in 6.14 but nothing recent.
- Vendors have accumulated patches — Honor’s anonymous-reclaim hack and Google’s Android hook exempting foreground tasks are both out-of-tree.
- Kairui Song (SUSE) proposes a redesign that moves the gen-tracking out of
folio->flagsto free three flags. This would enable 63 generations and solve the page-flag scarcity argument, plus enable better history tracking that addresses the anon/file balance problem. - Kalesh Singh (Google) raised compatibility issues — Android’s user-space OOM daemon assumes the metrics mglru emits, complicating the user-space story.
The two competing implementations (mglru vs classic active/inactive) both ship, both compile, both can be runtime-toggled. The state is: mglru is in mainline, kconfig-default-off, can be turned on, has unresolved problems, and is being debated for removal.
That’s the state. Don’t read the bold “merged in 6.1” lines and assume it’s healthy. Read the discussion threads on lkml for the working state of any version.
How to verify your kernel is using mglru
# Boot with the config on
cat /proc/config.gz | gunzip | grep CONFIG_LRU_GEN
# CONFIG_LRU_GEN=y
# CONFIG_LRU_GEN_ENABLED=y (this is the boot-time default for mglru on/off)
# CONFIG_LRU_GEN_STATS=n (optional: keeps historical stats)
# Check runtime
cat /sys/kernel/mm/lru_gen/enabled
# 0x0007 if mglru is enabled and doing batch-clearing
# Sanity: does this kernel expose the runtime ABI?
ls /sys/kernel/mm/lru_gen/enabled
# if no such file: mglru is built off entirely or your kernel is pre-6.1Note that CONFIG_LRU_GEN=y makes the code compile, but the runtime default on most distribution kernels is mglru-disabled. Ubuntu 24.04 LTS, RHEL 9.x, and most cloud-provider kernels ship with CONFIG_LRU_GEN_ENABLED=n. The ChromeOS kernel and Google’s downstream Android forks turn it on. The Debian experimental kernel has it on. Google’s production servers are running it.
If you’re on a stock distribution kernel, you’re likely on classic active/inactive LRU. To enable, recompile with CONFIG_LRU_GEN_ENABLED=y (or set it via kpatch without reboot — but that’s not in mainline yet).
Walkthrough of the code paths
If you read the kernel source:
mm/vmscan.cis where most of the logic lives. Look forlru_gen_*prefix.mm/mmu_gather.c(ormm/rmap.c, depending on kernel) handles rmap walks.include/linux/mm_inline.hhas thelru_gen_folio_idx()helper and the bit definitions for generation flags.Documentation/admin-guide/mm/multigen_lru.rstis the upstream doc — sparse but authoritative. Runmake htmldocsto render locally.
A useful kernel-debugging trick: enable CONFIG_LRU_GEN_STATS=y to get /sys/kernel/debug/lru_gen_full, which keeps history for evicted generations and lets you reconstruct what the algorithm saw across a window. That interface is currently experimental and not ABI-stable — but it gives you ground truth when tuning.
A practical recipe for production
For a vanilla cloud Linux host running containerized workloads:
- Leave
CONFIG_LRU_GEN_ENABLED=non your default kernel unless you have a reason. - If you flip it on (e.g. you’re chasing tail-latency or MongoDB-style random-read wins), monitor specifically for:
- Memory growth on anonymous-heavy pods. Watch
memory.currentand your cgroup’s anon-vs-file ratio (memory.stat). memory.events.highgoing up on cgroups that aren’t at budget — over-reclaim signature.- PSI memstall in the system-wide metric, not cgroup. If mglru turns recovery into a thrash, that line will spike.
- Memory growth on anonymous-heavy pods. Watch
- Pair with
min_ttl_ms=1000if the host is interactive-facing; leave it at 0 on backend batch. - Don’t combine with zswap on a tight-memory system if you’re chasing tail-latency; the interactions between zswap, mglru, and PSI are still being tuned downstream.
For a single-tenant workload (one large database, one machine):
- If you’ve validated through a benchmark (fio, YCSB, sysbench) that mglru helps, it’s a fine choice.
- Don’t trust vendor testimonials published against 5.15-era kernels; verify on your kernel version.
For a desktop / laptop:
- Enable mglru, enable
min_ttl_ms=1000, profit (or accept OOM kills when the working set drifts past physical RAM).
For a Linux kernel developer / maintainer in this area:
- The open problem is the anonymous/file balance, the over-reclaim from cgroups, and the page-flag budget. The deflag-from-LRU work (Kairui Song’s series) is worth watching; if it lands, both perf and the API surface improve.
Connections to the rest of memory management
mglru is one of three real-time signals about memory behavior in modern Linux:
| Signal | Where | What it tells you |
|---|---|---|
memory.pressure |
cgroup | PSI stall time on memory path |
memory.events.{high,max,oom} |
cgroup | count of reclaim-trigger invocations |
min_ttl_ms=... rejecting evict |
system | kernel chose OOM over evict; effectively a count, but not exported as one |
These three together let you know whether your mglru configuration is healthy or whether it’s racing.
On the question “should I use mglru?”
If you’ve read this far, here’s the principled answer:
- If you’re running a vendor-tuned, single-tenant workload that’s been benchmarked: yes, mglru.
- If you’re running mixed-container bare-metal: hold until the anon/file balance problem is resolved in mainline. The current behavior has been shown to leak memory on anonymous-heavy workloads.
- If you’re on a distribution kernel that ships with
LRU_GEN_ENABLED=n: the kernel maintainers upstream weren’t confident enough to flip the default. Trust that, until you have evidence otherwise. - If you’re doing kernel work: the deflag redesign is the most exciting direction. Watch that patch series.
The algorithm itself isn’t the wrong idea. Its first production iteration has well-defined issues, and the community is actively reconsidering whether the implementation in mainline is the one that should survive. That’s a healthy state for an in-tree algorithm; it’s also a state where you shouldn’t switch your production kernel without measurement.
## Related articles
This is one of three on Linux kernel memory management under tux.fan’s os-kernel tag.
– [Linux PSI memory pressure: what it really measures](https://tux.fan/2026/07/21/psi-memory-pressure/) — companion piece. Reclaim stalls light up `memory.pressure`; PSI is what you watch when mglru’s tuning gets noisy.
– [Linux memory cgroup v2: how the kernel enforces the budget](https://tux.fan/2026/07/21/memory-cgroup-v2/) — the budget model mglru is reclaiming against. The article covers the three-tier `low`/`high`/`max` limits and the per-memcg LRU model mglru walks.
Comments