Linux memory · live server audit · compressed swap

Zswap or zram on a small Linux server? A live audit

A live audit of zswap on a 5.63 GiB ARM server, why zram changes its failure mode, and how macOS takes a different VM-integrated path.

Verdict — keep zswap enabled. Leave LZO, zbud, and the 20% pool threshold alone. Keep /dev/sda2 active, and do not add zram as a higher-priority swap device. The current pool is tiny, and this boot has recorded no pool-limit hits, cached-page writeback, or rejected stores.

The caveat matters. This server runs Linux 5.4. Modern architectural advice about zswap still helps, but current-kernel allocator, shrinker, and cgroup commands do not transfer directly.

Start with the failure mode, not the compressor

Run a low-memory Linux server with real swap and wonder whether zram would be better? Decide from the machine’s topology, kernel version, and live counters instead of copying a tuning recipe.

The reusable question is not “Which compressor benchmarks fastest?” It is “What happens when compressed memory fills?”

Zswap is a compressed cache in front of an ordinary swap device. Suitable pages stay compressed in RAM; pages that bypass the cache or age out can still go to disk. The disk remains a slower capacity tier.

Zram is a compressed RAM block device. When it is used as swap, its storage still consumes finite physical memory. A zram-only design without a configured zram backing device therefore ends at an in-RAM capacity boundary. Adding lower-priority disk swap does not, by itself, make zram an age-aware cache that automatically moves its oldest pages to disk.

Two memory-pressure topologies: zswap keeps a disk-backed capacity tier, while zram-only without a configured backing device ends at compressed RAM.
The current topology can degrade from compressed RAM to disk. The illustrated zram-only topology has no configured zram backing device, so it changes that failure mode rather than merely changing the compressor.

Zram can optionally use an explicitly configured backing device and administrator-triggered writeback on kernels built with that feature. That is a separately managed topology: it does not turn ordinary lower-priority swap into automatic zswap-style eviction, nor does it extend zram’s finite logical device size.

That distinction makes zswap the conservative fit for this server: it already has an 8 GiB swap partition, no zram device, and no requirement to keep swap data off persistent storage.

The host already has a coherent zswap setup

The boot command line contains zswap.enabled=1, and the same argument is present in the default grubby kernel stanza. The runtime switch reports Y. The kernel log closes the loop:

zswap: loaded using pool lzo/zbud

Only the enable flag was added explicitly. LZO, zbud, the 20% threshold, and same-filled-page handling are Linux 5.4 defaults exposed through sysfs.

PropertyObserved valueWhy it matters
Operating systemOracle Linux Server 7.9An older server distribution, not a current zram-first desktop setup.
Kernel5.4.17-2102.206.1.el7uek.aarch64Modern zswap commands must be checked against this implementation.
Base page size65,536 bytesEvery stored_pages unit is 64 KiB, not the 4 KiB common on x86.
Physical RAM5,903,296 KiB (5.63 GiB)The 20% pool threshold is about 1.126 GiB.
Backing swap/dev/sda2, 8 GiB, priority -2Zswap has a real device for bypass and eviction.
ZswapLZO · zbud · 20%The pool grows on demand; 20% is not reserved at boot.
VM policyswappiness=60, page-cluster=3Distribution defaults remain in place.
zramNo deviceThere is no competing compressed swap tier.
Userspace OOM policyNone detectedNeither systemd-oomd nor earlyoom is active.

The 20% value is easy to misread. Zswap does not seize 1.126 GiB when the machine boots. Its pool grows only as pages are accepted and shrinks when entries are loaded, invalidated, or written to the backing device.

The counters show light use, not a tuning problem

After 6 days and 5 hours, debugfs reported:

CounterValueInterpretation
stored_pages89Eighty-nine 64 KiB pages currently have zswap entries.
same_filled_pages1One page is represented as a repeated value with no compressed payload.
pool_total_size2,883,584 bytesThe zbud pool occupies 2.75 MiB, or 44 physical 64 KiB pages.
pool_limit_hit0No store recorded finding the aggregate pool over the configured threshold.
written_back_pages0No cached page has been evicted to disk during this boot.
All rejection counters0No poor-compression, allocation, reclaim, or metadata failure was recorded.

The page size makes the accounting unusually clear:

89 stored pages × 64 KiB                 = 5.5625 MiB logical pages
2,883,584 bytes of zbud storage          = 2.75 MiB physical pool
2.75 MiB ÷ 64 KiB                        = 44 physical pool pages
(89 entries − 1 same-filled entry) ÷ 44 = 2 ordinary entries per pool page

The readings are consistent with the 88 ordinary entries fitting two per zbud page. Including the same-filled page, zswap represents about 5.56 MiB of logical pages with 2.75 MiB of pool memory—a little over 2:1 before entry metadata.

The snapshot contains 5.56 MiB of logical pages in a 2.75 MiB pool, only 0.24% of the configured threshold, with zero writeback and rejection events.
The live pool is tiny relative to its threshold. That is good operational news and weak tuning evidence.

This is logical-to-pool accounting, not a universal LZO compression ratio. Zbud caps density at two entries per physical page, the debugfs files were read separately rather than atomically, and the counters are approximate under concurrent activity.

The traditional page-swap I/O counters and the partition’s completed writes were also zero since boot. The defensible conclusion is narrow: zswap is active, currently holds a few megabytes, and has not approached a failure boundary. The snapshot does not prove a measurable application speedup.

One swapped page gets a fast path and an escape hatch

A page selected for reclaim follows this path on Linux 5.4:

  1. The page has or obtains a swap entry identifying a slot on /dev/sda2.
  2. Zswap gets the first opportunity to keep it in compressed RAM. A same-filled page can be represented by metadata alone; an ordinary page is compressed and placed in zbud if it fits.
  3. If zswap is disabled, over its limit, unable to reclaim space, or unable to store the compressed result, the normal disk-swap path remains available.
  4. On a page fault, zswap reconstructs or decompresses the page and removes the cache entry. If the page is no longer cached, ordinary swap-in handles it.

The backing slot still matters while the payload is in zswap: it gives the page a swap identity and reserves somewhere to write it later. That is why swapon --show can report used swap without proving that the same number of bytes reached the disk.

Replacing zswap with zram would change the operating model

A zram conversion is not a drop-in compressor change. It replaces a cache over durable capacity with a block device whose storage consumes the same RAM the system is trying to relieve.

LayoutWhat happens under pressureFit for this host
Current: zswap + disk swapSuitable pages stay compressed in RAM; bypassed or older pages can use the 8 GiB partition.Keep. It preserves a capacity tier, and the counters show no failure requiring redesign.
High-priority zram + lower-priority disk swapOnce zram’s logical swap slots are exhausted, later swap allocations can use disk while older pages remain in zram. A compressed-storage allocation failure is an I/O failure, not automatic reassignment of that page to disk.Avoid as a casual “faster swap” change.
Zram as the only swap device, with no zram backing deviceSwap stays in RAM, but the device has finite logical capacity and consumes finite physical memory.Coherent only when diskless operation, persistence constraints, or deliberate disk-swap avoidance justify the new boundary.
No swapCold anonymous memory loses its backing tier; file cache and kill policy absorb pressure sooner.No evidence from this audit supports removing the existing safety margin.

Zram can be the right design for embedded, diskless, or persistence-sensitive systems. This server has none of those stated requirements. It also lacks a proactive userspace OOM manager, so removing disk-backed capacity would require an explicit pressure and kill policy rather than a one-line swap substitution.

With compressor swap, macOS is topologically closer to zswap than zram

When its backing tier is active, macOS and zswap both keep non-discardable pages compressed in RAM before slower storage. Zram is instead a compressed block device. This is a topology analogy, not an implementation equivalence.

Linux starts with swap identity. A page selected for swap owns a slot on /dev/sda2, and Frontswap gives zswap a chance to cache that page before I/O. The slot remains reserved while its compressed payload lives in zbud. If zswap evicts the entry, Linux reconstructs one full page and writes it through the ordinary swap path.

XNU starts inside the Mach VM object and pager path. Dirty reclaimable pages from internal objects enter the compressor queue; the kernel creates a compressor pager for the object when needed, and vm_compressor_pager_put() stores the page. On a later fault, the pager retrieves and decompresses it. Dirty external-object pages go to their external pager for cleaning; already-clean file-backed pages can normally be reclaimed and faulted back from that pager. Wired pages are not compressor candidates.

The storage geometry is also different. This Linux host’s zbud allocator can place at most two compressed entries in one 64 KiB pool page. XNU packs compressed slots into segments, compacts sparse segments, ages them, and—when its compressor-with-swap mode and backing storage are available—can move eligible segments to swap files. A fault into a swapped segment first brings its populated extent back before the requested slot is decompressed. In short: zswap caches individual pages that already have disk-swap identities; XNU’s compressor pagers map internal-object offsets into a globally managed tier of compressed segments.

PropertyLinux 5.4 zswap on this VMmacOS compressor
VM integrationFrontswap intercepts the normal swap store and load path.A Mach memory-object pager is part of pageout and fault handling.
In-RAM unitOne compressed page entry in a zpool, tied to one ordinary swap offset.A pager maps each object offset to a compressor slot; payloads from different mappings are packed into globally managed segments.
Backing-storage unitAn evicted entry is decompressed to a full page and written to its reserved swap slot.Under compressor-swap policy, eligible segments are written and read as their page-rounded populated extent.
Allocation geometryzbud: at most two entries per 64 KiB pool page.Variable compressed slots packed into compactable segments; no two-entry ceiling.
Observed base page size64 KiB on the ARM Linux guest.16 KiB on the Apple Silicon Mac sampled separately.
Compression policyLZO and zbud are visible, selectable kernel parameters.Published source defines WKdm-only, LZ4-only, and adaptive hybrid modes. The sampled release kernel did not expose its active mode.
Operator interfacePool limits, compressor, allocator, and debug counters are visible in sysfs and debugfs.macOS presents memory pressure, compressed memory, wired memory, and swap use; its allocator policy is not a supported tuning surface.

Two live ratios, but not a benchmark

A separate Apple Silicon snapshot on macOS 26.6.1, captured with sysctl and vm_stat, reported a 16 KiB page size, 620,095 pages stored in the compressor, and 224,248 pages occupied by it:

Linux: 89 × 64 KiB ÷ 2.75 MiB       ≈ 2.02:1 logical-to-pool
macOS: 620,095 × 16 KiB              ≈ 9.46 GiB stored-page bytes
       224,248 × 16 KiB              ≈ 3.42 GiB resident compressor-data pages
       9.46 GiB ÷ 3.42 GiB           ≈ 2.77:1 stored-page bytes / data-page bytes

The Mac simultaneously reported zero current encrypted swap use and zero cumulative swap-in and swap-out events. That does not mean macOS lacks a backing tier: XNU defines both compressor-only and compressor-with-swap configurations. It means only that this snapshot’s stored pages were not disk-resident. In general, vm_stat’s current “stored” count can include logical pages whose segments are in swap, while “occupied” counts resident physical data pages; neither includes every compressor metadata allocation. Those gauges must not be confused with the cumulative compression, decompression, swap-in, and swap-out event counters.

The apparent density advantage is not a codec contest. The samples came from different workloads at different times; their base pages differ by 4×; zbud’s two-entry layout constrains the Linux pool; XNU packs and compacts variable-size slots; and the two kernels account for metadata differently. The Linux numerator includes one same-filled page with no zbud payload, while XNU’s stored-page accounting includes analogous single-value pages. Neither allocator-footprint ratio says which system has lower fault latency or whether either compressor is optimally sized.

Do not copy a macOS result into a Linux tuning target

The useful lesson is architectural, not numerical. macOS makes compression part of an integrated pressure policy, while Linux exposes replaceable components and thresholds. On this VM, a few megabytes in zswap and zero failure events leave no density problem to solve. Application memory, tail latency under real pressure, and the age of the 5.4 kernel remain the higher-value targets.

The source comparison uses XNU 12377.121.6, the nearest published release to the sampled Mac’s later 12377.161.13 kernel. In that tag, ARM macOS defaults the compressor payload buffer to 64 KiB, overridable by a boot argument. This is not a 64 KiB VM page: on the sampled 16 KiB-page Mac, the resident or swapped populated extent is page-rounded and can occupy up to four base pages. Apple’s source describes the size as a tradeoff: larger segments reduce end slack and overhead and improve I/O throughput, while smaller ones reduce unnecessary swap-in of data that is not subsequently accessed.

Linux 5.4 is the boundary on modern advice

Chris Down’s 2026 zswap-versus-zram analysis describes a much newer memory-management implementation, using Linux 6.19 examples. The architecture-level lesson transfers: zswap is a cache in front of disk, while zram is a finite compressed block device. The tuning details do not all transfer.

AreaThis Linux 5.4 hostModern zswap
Pool storagezpool with zbud; at most two compressed entries per physical pool page.Direct zsmalloc integration can provide denser storage.
Pool pressureA store that finds the static limit exceeded attempts synchronous reclaim and may reject the new cache entry.Acceptance hysteresis, an optional pressure-aware shrinker, and newer reclaim behavior are available.
Cgroup controlNo memory.zswap.* interface in the upstream 5.4 path.Per-cgroup zswap accounting and writeback controls are part of the current design.
Operational adviceUse the interfaces present on the running host.Current commands assume current kernel support.

Do not switch this kernel to zsmalloc by analogy

The Linux 5.4 documentation explicitly warns that its zsmalloc zpool does not implement compressed-page eviction. Once that pool fills, zswap can only reject new pages. The presence of CONFIG_ZSMALLOC=y therefore does not make zsmalloc the correct pool for this old zswap implementation.

The same rule applies to modern shrinker parameters and per-cgroup writeback controls: do not copy commands for interfaces that do not exist on the running kernel.

The safe plan is keep, measure, then modernize

1. Keep the current topology

  • Leave zswap.enabled=1, LZO, zbud, and the 20% threshold unchanged.
  • Keep the 8 GiB backing swap partition active.
  • Do not add zram beside it as a supposedly faster first swap device.
  • Keep the boot argument in the kernel-management path and verify it after maintenance. It appears in the generated grubby stanza, not /etc/default/grub.

2. Measure a real memory-pressure event

A 2.75 MiB pool with zero failure events is good operational news and poor tuning evidence. The next useful sample is the same counter set captured before, during, and after a real deployment, traffic spike, backup, or worker-memory event—not an artificial swapoff on a remote production machine.

Watch these signals together:

ObservationLikely reading
stored_pages and pool_total_size grow; writeback stays flatThe compressed cache is absorbing swap candidates.
written_back_pages grows without repeated swap-insCold-page tiering may be working normally.
Writeback, swap-ins, storage I/O, and tail latency all rise quicklyThe host may be thrashing.
Pool-limit or rejection counters riseThe pool is reaching a real constraint and tuning evidence now exists.

Application latency and storage I/O matter more than any one zswap counter. Compare deltas over an interval rather than treating a static value as a performance result.

3. Modernize the platform, not one old knob

The largest technical gap is the 5.4 memory-management implementation. A supported operating-system and kernel upgrade can bring newer zswap storage, cgroup accounting, pressure-aware reclaim, counters, and maintained security fixes together. Validate that change during planned maintenance; do not approximate it by changing the allocator underneath the old kernel at runtime.

Reproduce the audit without changing memory policy

The following commands are read-only:

printf '%s\n' '=== host ==='
uname -a
cat /etc/os-release
getconf PAGE_SIZE
cat /proc/cmdline

printf '%s\n' '=== memory and swap ==='
free -h
cat /proc/swaps
grep -E '^(MemTotal|MemAvailable|SwapTotal|SwapFree|SwapCached):' /proc/meminfo

printf '%s\n' '=== zswap parameters ==='
for file in /sys/module/zswap/parameters/*; do
  printf '%s=' "${file##*/}"
  cat "$file"
done

printf '%s\n' '=== zswap counters ==='
sudo sh -c '
  for file in /sys/kernel/debug/zswap/*; do
    printf "%s=" "${file##*/}"
    cat "$file"
  done
'

printf '%s\n' '=== VM policy ==='
for name in swappiness page-cluster watermark_scale_factor; do
  printf 'vm.%s=' "$name"
  cat "/proc/sys/vm/$name"
done

printf '%s\n' '=== zram and OOM policy ==='
zramctl 2>/dev/null || true
systemctl is-active systemd-oomd 2>/dev/null || true
systemctl is-active earlyoom 2>/dev/null || true

Read stored_pages using the machine’s actual base page size. Assuming 4 KiB on this ARM host would understate the logical pages by a factor of sixteen. Read pool_total_size as physical bytes used by compressed storage, not logical bytes saved.

What this snapshot cannot prove

  • No synthetic pressure was induced on the production server, so the audit does not benchmark a full pool.
  • Debugfs event counters are intentionally approximate under concurrent access.
  • The snapshot cannot attribute swap candidates to individual processes or cgroups through this old zswap interface.
  • There is no zswap-disabled or zram comparison run, so the audit cannot assign a performance gain to the current 2.75 MiB pool.
  • The macOS snapshot is a separate machine and workload, so its compressor density cannot benchmark or size this Linux host.
  • Upstream Linux 5.4 explains the observed interfaces, but Oracle UEK may carry additional patches; runtime behavior remains authoritative for this host.

The evidence supports a no-change decision, not a claim of optimality. Zswap is enabled, correctly backed, lightly used, and has no recorded failure events. The old kernel is the larger operational constraint, which makes a planned platform upgrade more defensible than changing the compressed-swap topology without a workload requirement.

How Linux 5.4 handles a full zbud pool

Show the full-pool reclaim sequence

Upstream Linux 5.4 checks aggregate zswap pool size before each store. If the pool is already over max_pool_percent, zswap synchronously asks the selected zpool to free one physical page.

With zbud, reclaim starts at the tail of the pool-page LRU and can inspect up to eight candidate pages. For each candidate it tries occupied buddies in order, stopping that candidate at the first failed writeback callback. A page is freed only when both buddies are gone; if the first succeeds and the second fails, the page remains partly evicted and reclaim proceeds to another candidate.

The incoming store is rejected if reclaim fails or the aggregate pool remains over the threshold. Because the check happens before allocation and stores are not serialized around a byte-exact cap, 20% is a store-time threshold rather than a strict ceiling.

Sources

  1. Chris Down, “Debunking zswap and zram myths”, 24th March 2026.
  2. Linux kernel 5.4 documentation, “zswap”.
  3. Linux v5.4 source, mm/zswap.c.
  4. Linux v5.4 source, mm/zbud.c.
  5. Current Linux kernel documentation, “zswap”.
  6. Linux kernel documentation, “zram: Compressed RAM-based block devices”.
  7. Oracle, Unbreakable Enterprise Kernel Release 6 release notes.
  8. Apple Support, “View memory usage in Activity Monitor on Mac”.
  9. macOS vm_stat(1) manual.
  10. Apple open-source XNU 12377.121.6: pageout, compressor pager, segment management, backing store, and compression algorithms. This is the nearest published source to the sampled 12377.161.13 kernel, not its exact patch build.