zswap
zswap is a compressed write-behind cache that sits in front of a real swap device. When the reclaim path decides to swap an anonymous page out, zswap intercepts the page, compresses it into a dynamically allocated RAM pool, and — crucially — does not write it to the disk swap device yet. The page only reaches the backing swap device later, if zswap’s pool fills or the page proves cold or incompressible. The whole idea is to trade CPU cycles for swap I/O: compressing a 4 KiB page costs microseconds of CPU, while a fault into a disk swap device costs milliseconds — “the latency gap between RAM and swap, even on a fast SSD, can be four orders of magnitude” (Seth Jennings, the original zswap proposal, LWN, Feb 2013). zswap was authored by Seth Jennings; this note pins behavioural and configuration claims to Linux 6.12 LTS (2024-11-17) and 6.18 LTS (2025-11-30) and dates the differences (zswap admin-guide, v6.12; v6.18).
zswap is the cache member of the kernel’s compressed-memory pair; its sibling zram is a standalone compressed RAM block device. The single most important distinction — restated throughout this note — is that zswap always has a real backing swap device behind it and is a transparent cache in front of that device, whereas zram is the device and needs no backing store. See the comparison section below and Linux Swap Subsystem for where both sit in the swap path.
Mental Model — A Compressed Tier Between Reclaim and Disk
Think of zswap as a fast L1 cache inserted into the swap write path. Reclaim hands it a page; zswap tries to compress it; on success the compressed blob lives in RAM and the (slow) write to the swap device is avoided. The disk swap device becomes a slower L2 backing store that is touched only on pool overflow, on writeback of cold pages, or for pages zswap refuses (incompressible ones).
flowchart LR RECLAIM["Reclaim evicts an<br/>anonymous page"] -->|"swap_writepage"| ZSWAP{"zswap store<br/>(zswap_store)"} ZSWAP -->|"compresses well<br/>(< page size)"| POOL["Compressed pool<br/>(zsmalloc) in RAM"] ZSWAP -->|"same-filled<br/>(zero/repeat)"| SAME["Store value only,<br/>0 bytes of pool"] ZSWAP -->|"incompressible /<br/>pool full / rejected"| DISK[("Real swap device<br/>(disk / SSD / file)")] POOL -->|"pool full OR<br/>shrinker (cold pages)"| WB["Writeback:<br/>decompress + write"] WB --> DISK FAULT["Page fault on<br/>swap PTE"] -->|"zswap_load"| POOL FAULT -->|"miss"| DISK
zswap as a compressed tier in the swap write path. What it shows: every page reclaim wants to swap out passes through zswap_store; it lands in the in-RAM compressed pool (the fast path), is collapsed to a stored value if it is a same-filled page, or falls through to the real swap device if it is incompressible or the pool is over its limit. A later fault first probes zswap (zswap_load) and only reads the disk on a miss. The insight to take: zswap never replaces the swap device — it defers and often avoids writes to it. Anonymous memory still ultimately has a real backing store; zswap is a compression cache layered transparently in front of it. Contrast zram, which is itself the bottom of that diagram with no disk below it.
Why zswap Exists — The Frontswap Lineage
Swapping is the kernel’s mechanism of last resort for anonymous pages (heap, stack, anonymous mmap) that have no file to write back to. When reclaim must free such a page it has nowhere to put the contents except a swap device, and a swap device — even an NVMe SSD — is orders of magnitude slower than RAM. zswap’s bet is that for many workloads the data is compressible and the CPU is idle relative to the I/O bottleneck, so spending CPU to compress into RAM beats spending wall-clock time on disk I/O. Jennings’ original benchmark reported “a runtime reduction of 53% and an I/O reduction of 76% with zswap compared to normal swapping” on a memory-constrained system (LWN, 2013).
Mechanically, the original zswap hooked the frontswap API (in the kernel since Linux 3.5). Frontswap acted as “a ‘fronting store’ — a type of a cache — to an existing swap device”: under memory pressure “swapped pages instead go directly to zcache/zswap where they are compressed” rather than traversing the block-I/O subsystem, and on a fault “the block I/O subsystem is again bypassed and … the page is decompressed directly into the faulting page frame” (Dan Magenheimer, LWN, “In-kernel memory compression”, Apr 2013). The page still had a real swap slot reserved (so the swap accounting and the swap cache stayed consistent), but the data was diverted into zswap’s compressed pool instead of being written immediately. Over the years the explicit frontswap layer was removed and zswap was wired directly into the swap-out and swap-in code paths, but the shape is unchanged: a swap slot is reserved, the compressed copy lives in RAM, and the slot’s disk blocks are written only on writeback.
Uncertain
Verify: the exact mainline release in which zswap was first merged. zswap was proposed Feb 2013 (LWN); recall places the merge in the 3.11 cycle, but neither fetched LWN article states the merge release (537422 is a pre-merge proposal). Reason: merge-version claim is recall, not from a fetched primary. To resolve: check the
git logfirst appearance ofmm/zswap.c. uncertain
The benefits the admin-guide enumerates (v6.12 zswap.rst) follow directly: laptops with little RAM mitigate the cost of swapping; overcommitted VMs sharing a host I/O subsystem cut their swap I/O and dodge hypervisor I/O throttling; and SSD-swap users extend device life by “drastically reducing life-shortening writes.”
Mechanical Walk-through — Store, Load, Invalidate
zswap exposes three operations to the swap subsystem, visible directly in mm/zswap.c (v6.12):
Store (swap-out). When the swap subsystem is about to write a page out, it calls zswap’s store path. zswap first checks whether the page is a same-filled page — its 4 KiB contents are a single repeated value (the common case being an all-zero page). If so, zswap stores only the repeated value and a flag, allocating zero bytes of pool memory for the page’s data; this is the same-filled-page optimization (see below). Otherwise zswap compresses the page using its configured compressor through the kernel crypto acomp (asynchronous compression) interface — mm/zswap.c includes <crypto/acompress.h> and allocates a per-CPU crypto_acomp_ctx so each core compresses without contending on a shared transform. If the compressed output is smaller than a page and the pool is under its limit, zswap allocates a handle from the compressed pool (zsmalloc — see zram for the zsmalloc/zspage design) and stores the blob. It then records the mapping from the swap entry (the (swap_type, swap_offset) pair that identifies a swap slot) to the pool handle. As of 6.12/6.18 this mapping is “a red-black tree per swap type” keyed on the swap offset (zswap.rst).
If compression fails, the output is not smaller than a page (incompressible data), or the pool is over max_pool_percent, zswap rejects the page and lets the normal swap path write it to the real device uncompressed. This rejection is why zswap “preserves RAM and CPU cycles” on incompressible data rather than wasting the pool on it (Chris Down, 2026).
Load (swap-in). On a page fault against a PTE that is a swap entry, the swap-in code calls zswap’s load function. zswap looks up the swap entry in the red-black tree; on a hit it decompresses the blob (or reconstructs the same-filled value) into the page the fault handler just allocated, and the disk is never touched. On a miss the data is on the real swap device and the normal swap-in reads it from disk.
Invalidate. “Once there are no PTEs referencing a swap page stored in zswap (i.e. the count in the swap_map goes to 0) the swap code calls the zswap invalidate function to free the compressed entry” (zswap.rst). This keeps the pool from accumulating dead entries when a process exits or MADV_DONTNEEDs a region.
Pool overflow and LRU writeback. zswap maintains its own LRU of stored pages. When the pool reaches its size limit, zswap “evicts pages from compressed cache on an LRU basis to the backing swap device” — it decompresses the coldest entries and writes them out to their reserved swap slots, freeing pool memory. This is the L1→L2 spill: the page leaves fast compressed RAM and lands on the slow disk it always had a slot reserved on.
The Same-Filled-Page Optimization
A surprisingly large fraction of anonymous memory in real workloads is all zeros (freshly mmaped-but-touched pages, zeroed buffers) or a single repeated byte/word pattern. zswap detects these before compressing: “a page is checked if it is a same-value filled page before compressing it. If true, the compressed length of the page is set to zero and the pattern or same-filled value is stored” (zswap.rst). A same-filled page therefore consumes essentially no pool memory — just a small metadata entry recording the repeated value — and reconstructs instantly on load by memset-ing the value across the page. This is distinct from compression: even a perfect compressor would still allocate some pool bytes for a zero page, whereas the same-filled path allocates none. The debugfs interface exposes a counter for same-value filled pages.
Configuration — Knobs as of 6.12 / 6.18
zswap is configured through module parameters under /sys/module/zswap/parameters/ and matching zswap.* kernel command-line options. Below, each is annotated with its default and the kernel that backs the claim.
# Is zswap active at boot? This depends on the BUILD-TIME Kconfig
# CONFIG_ZSWAP_DEFAULT_ON (see the warning below), overridable at boot:
zswap.enabled=1 # kernel cmdline: force on
echo 1 > /sys/module/zswap/parameters/enabled # toggle at runtime
# Maximum percentage of total RAM the compressed pool may occupy.
# Default 20 (per mm/zswap.c v6.12: zswap_max_pool_percent = 20).
echo 20 > /sys/module/zswap/parameters/max_pool_percent
# Compressor. Default selected by CONFIG_ZSWAP_COMPRESSOR_DEFAULT,
# whose upstream default choice is LZO in both 6.12 and 6.18.
echo zstd > /sys/module/zswap/parameters/compressor
# Hysteresis: once the pool hits max_pool_percent, refuse new pages
# until usage falls back below this percent. 100 disables hysteresis.
echo 90 > /sys/module/zswap/parameters/accept_threshold_percent
# Dynamic shrinker: proactively write cold pool pages back to swap
# under memory pressure. Disabled by default (CONFIG_ZSWAP_SHRINKER_
# DEFAULT_ON default n in both 6.12 and 6.18).
echo Y > /sys/module/zswap/parameters/shrinker_enabledLine-by-line:
enabled— turns storing of new pages on or off. Disabling at runtime does not flush the pool: “it will not immediately write out or fault back into memory all of the pages stored in the compressed pool” — existing entries stay until invalidated or faulted in; aswapoffis the way to force them all back (zswap.rst).max_pool_percent— the only “policy” knob the docs advertise; the cap on pool size as a percentage of total RAM, default 20% (zswap_max_pool_percent = 20inmm/zswap.c, v6.12).compressor— any algorithm registered with the crypto acomp interface (lzo,lz4,lz4hc,zstd,deflate,842). Changing it at runtime leaves already-stored pages in their old pool, decompressed with their original compressor, until they drain (zswap.rst). The LWN compression-benchmark page referenced from Kconfig (LWN, 2018) lays out the trade-off concretely: LZ4 and LZO favour speed at a lower compression ratio (high write throughput), while deflate and zstd reach higher ratios at much lower throughput (zstd’s speed dropping sharply at high levels) — so the right choice depends on how CPU-bound vs. I/O-bound the workload is.accept_threshold_percent— hysteresis. Without it, a full pool would flip pages in and out repeatedly “without any real benefit but with a performance drop”; the threshold makes zswap refuse new pages until usage drops back below it (zswap.rst).shrinker_enabled— the dynamic shrinker. By default zswap only writes back when the pool is full; with the shrinker on, zswap proactively writes cold pool pages back to swap under memory pressure even before the pool fills, “reducing the chance that cold pages will reside in the zswap pool and consume memory indefinitely” (mm/Kconfig). The shrinker registers with the kernel’s shrinker framework (see Shrinkers and Slab Reclaim) and scales eviction with observed pressure.
A per-cgroup knob, memory.zswap.writeback, lets an admin disable writeback (but not zswap itself) for a cgroup: echo 0 > /sys/fs/cgroup/<name>/memory.zswap.writeback. The docs warn that with recurring store failures (e.g. incompressible pages) this causes “reclaim inefficiency” because the same pages are rejected again and again (zswap.rst). This ties zswap into The Memory Cgroup memcg.
Uncertain
Verify: zswap is NOT unconditionally on by default in upstream 6.12 or 6.18. In both trees
CONFIG_ZSWAP_DEFAULT_ONis a plainboolwith nodefault y(6.12 mm/Kconfig; 6.18 mm/Kconfig), so a vanilla build leaves zswap disabled at boot unless the builder setsCONFIG_ZSWAP_DEFAULT_ON=yor the boot line passeszswap.enabled=1. Several distributions (notably Fedora) ship with it enabled, which is the likely source of the “default on” belief. Reason: the task brief’s “default-on as of 6.12/6.18” hint is a distro fact, not an upstream one. To resolve for a specific system: checkcat /sys/module/zswap/parameters/enabledand the running kernel’sCONFIG_ZSWAP_DEFAULT_ON. uncertain
The Compressed Pool — zpool (6.12) vs zsmalloc-only (6.18)
This is the headline behavioural difference between the two LTS kernels, so it gets its own section.
In 6.12, zswap obtained its pool through an indirection layer called zpool, and the allocator behind zpool was selectable. config ZSWAP did select ZPOOL, and a Kconfig choice picked the default allocator — default ZSWAP_ZPOOL_DEFAULT_ZSMALLOC if MMU (else zbud) (6.12 mm/Kconfig). Three allocators existed: zbud (“allocates exactly 1 page to store 2 compressed pages, which means the compression ratio will always be 2:1 or worse”), z3fold (three compressed pages per page), and zsmalloc (the dense allocator, “a more complex compressed page storage method [that] can achieve greater storage densities”). The pool/allocator could be changed at boot (zswap.zpool=zbud) or at runtime (echo zbud > /sys/module/zswap/parameters/zpool) (6.12 zswap.rst).
In 6.18, the zpool indirection and the zbud/z3fold allocators are gone: config ZSWAP now does select ZSMALLOC directly (6.18 mm/Kconfig), and the admin-guide is rewritten to say “Zswap makes use of zsmalloc for the managing the compressed memory pool” with the entire zpool attribute, the zswap.zpool= option, and the zbud/zsmalloc-density paragraph removed (6.18 zswap.rst — confirmed by diffing against the 6.12 doc). The rationale follows from the allocators’ own properties (documented in the 6.12 doc): zbud’s hard 2:1 ceiling and z3fold’s lower density were not worth maintaining once zsmalloc — “a more complex compressed page storage method [that] can achieve greater storage densities” — was the clearly superior dense allocator.
Uncertain
Verify: the community/maintainer rationale for removing zbud and z3fold from zswap is paraphrased from the allocators’ documented density properties, not from a fetched design-discussion thread. Reason: the removal-motivation discussion (mailing-list/LWN) was not fetched. The fact of the removal is verified (6.12 vs 6.18 Kconfig/docs diff); only the stated motivation is inferred. To resolve: read the removal commit message and its cover letter. uncertain
Uncertain
Verify: the exact intermediate release in which zbud/z3fold and the
zpoollayer were removed from zswap. Primary evidence brackets it cleanly — present and selectable in 6.12 LTS, gone (zsmalloc-only) in 6.18 LTS — but the precise release between them is from secondary sources (forum/wiki reports point at the 6.1x series). Reason: the changelog commit was not fetched. To resolve:git log --oneline v6.12..v6.18 -- mm/zswap.c mm/Kconfigand find theselect ZPOOL→select ZSMALLOCcommit. uncertain
Failure Modes and Diagnosis
- Incompressible workload → no benefit, wasted CPU. If the swapped data is already-compressed (media, encrypted blobs), zswap compresses, fails to shrink it below a page, rejects it, and sends it to disk anyway — having spent CPU for nothing. The debugfs reject counters (under
/sys/kernel/debug/zswap/) surface this; high reject counts mean the workload is a poor fit. - Pool thrash without hysteresis. With
accept_threshold_percent=100(hysteresis disabled) a full pool can flip pages in and out repeatedly, burning CPU. The default hysteresis exists precisely to prevent this. - Cold pages pinned in the pool. Without the shrinker, the pool only writes back when full, so cold pages can sit in compressed RAM indefinitely, holding memory that other uses could reclaim. Enabling
shrinker_enabledis the fix; this was the motivation for the dynamic shrinker. - Double-counting confusion. Because zswap reserves a real swap slot for every stored page, swap usage statistics can look high even though the data lives in RAM. The bytes are not on disk until writeback. Reading the debugfs pool/stored-page counters disambiguates.
Alternatives and When to Choose Them
The direct alternative is zram — a compressed RAM block device used as a swap device. The defining trade-off:
- zswap requires a real swap device and is a cache in front of it; zram is the device and needs no disk. zswap therefore degrades gracefully: cold and incompressible pages spill to the real disk. zram has a fixed
disksizeand, when full, has no automatic spill — pages either go to a lower-priority swap (risking LRU inversion) or the box OOMs (Chris Down, 2026). - zswap rejects incompressible pages to disk; zram stores them anyway, wasting RAM and CPU on data that did not compress.
- zswap’s writeback is integrated with reclaim (and the dynamic shrinker); zram’s writeback is a manual, snapshot-driven affair requiring a configured backing device and admin scripting.
Chris Down’s 2026 summary is blunt: “If in doubt, prefer to use zswap. Only use zram if you have a highly specific reason to” — reserving zram for diskless/embedded systems (e.g. Android) or setups that must keep swapped data off persistent storage. See zram for the cases where it wins.
Production Notes
zswap shines on overcommitted virtual machines and memory-constrained laptops, exactly the cases Jennings’ original benchmark and the admin-guide call out. On servers it integrates cleanly with cgroup v2 memory control via memory.zswap.max (the per-cgroup pool cap) and memory.zswap.writeback. A common production tuning is to raise max_pool_percent above the conservative 20% default on workloads known to be compressible, pick zstd for ratio or lz4 for latency, and enable the shrinker so cold pages do not pin RAM. Because zswap keeps a real swap device behind it, it is the lower-risk default for general fleets — there is always somewhere for pages to go.
See Also
- zram — the sibling: a compressed RAM block device (no backing store); contains the detailed zsmalloc/zspage allocator explanation this note cross-links.
- Linux Swap Subsystem — where zswap intercepts the swap-out/swap-in paths; swap slots, the swap map.
- The Swap Cache — the page-cache-side structure that mediates swap I/O; zswap reserves real swap slots that stay consistent with it.
- Swappiness and Reclaim Balance — how reclaim decides to swap anonymous pages at all (the input to zswap’s store path).
- Memory Reclaim Overview, Shrinkers and Slab Reclaim — the reclaim machinery zswap’s dynamic shrinker plugs into.
- The Memory Cgroup memcg —
memory.zswap.max/memory.zswap.writebackper-cgroup controls. - Anonymous vs File-Backed Memory — why only anonymous pages go to swap (and thus through zswap).
- MOC: Linux Memory Management MOC (§10, Swap/zswap/zram).