DEV Community

Cover image for Compressed Swap: zram or zswap?
Mustafa ERBAY
Mustafa ERBAY

Posted on Originally published at mustafaerbay.com.tr

Compressed Swap: zram or zswap?

No corner of server discussions surrenders to slogans faster than swap. On one side, "turn swap off, RAM is cheap"; on the other, people blaming swap after every freeze. Both make the same mistake: they treat swap as a single thing.

In a modern kernel, compressed swap exists as two separate mechanisms, and because their names look alike they get confused constantly. zram is a compressed block device; you put swap on it and it never touches the disk. zswap is a compressed cache in front of your existing swap area; it catches pages before they go to disk.

The thesis of this article is simple: choosing between them isn't a matter of taste, it's an architectural decision determined by the machine's disk situation. And neither of them creates memory — they trade CPU for disk.

Neither is present on my server, and the reason is interesting

Let's start with my server running Ubuntu 24.04, because the picture is typical:

$ modinfo zram
modinfo: ERROR: Module zram not found.

$ apt-cache policy linux-modules-extra-$(uname -r)
linux-modules-extra-6.8.0-138-generic:
  Installed: (none)
  Candidate: 6.8.0-138.138

$ grep -E "CONFIG_ZSWAP|CONFIG_ZSWAP_DEFAULT_ON" /boot/config-$(uname -r)
CONFIG_ZSWAP=y
# CONFIG_ZSWAP_DEFAULT_ON is not set

$ cat /sys/module/zswap/parameters/enabled
N
Enter fullscreen mode Exit fullscreen mode

There are two different kinds of "absent" here. zram really is absent: on Ubuntu the module ships in a separate package (linux-modules-extra) which isn't installed on this machine. zswap, on the other hand, is compiled into the kernel but off by default — one line turns it on.

That distinction matters in practice. Someone saying "let me try zram" on this server starts with a package installation; someone saying "let me try zswap" starts with an echo. That's two very different preparation costs on the same machine.

They don't do the same job

Broadly speaking, both say "compress the swap page", but architecturally they sit in different places.

zram, in the kernel documentation's words, creates compressed RAM-based block devices: pages written to these disks are compressed and stored in memory itself. So /dev/zram0 is an ordinary block device as far as you're concerned; you run mkswap on it, say swapon, and your system's swap area now lives inside RAM in compressed form. It has no relationship with the disk — unless you want one.

zswap isn't a device but a cache. As the documentation puts it, it's a lightweight compressed cache that takes pages in the process of being swapped out and attempts to compress them into a dynamically allocated RAM-based pool. When the pool reaches its size limit, it evicts pages on an LRU basis to the backing swap device. So zswap needs a swap area on disk to work; it's a buffer in front of that area.

Diagram

The operational consequence: zram makes swap possible on machines with no disk, or whose disk you'd rather not wear out. zswap makes the swap you already have less painful.

zram: setting up the device and measuring it

The zram flow is short. Load the module, size the device, pick an algorithm, put swap on it:

modprobe zram num_devices=1
echo zstd > /sys/block/zram0/comp_algorithm
echo 4G   > /sys/block/zram0/disksize
mkswap /dev/zram0 && swapon --priority 100 /dev/zram0
Enter fullscreen mode Exit fullscreen mode

To add a device at runtime, reading /sys/class/zram-control/hot_add returns a new device id, and hot_remove takes one away. You see the available algorithms by reading comp_algorithm — the selected one appears in brackets.

For sizing, the documentation has its own rule and it's eminently sensible: since a 2:1 compression ratio is expected, there's little point creating a zram larger than twice the size of memory. If you want a hard guarantee anyway, mem_limit caps how much memory the device may use.

The real work starts after setup: measuring. The mm_stat file gives you the truth in a single line: orig_data_size is the uncompressed data size, compr_data_size its compressed form, mem_used_total the memory actually allocated including fragmentation, same_pages the identical pages that need no storage at all, and huge_pages the incompressible ones.

That last field is the number anyone running zram in production needs to watch. An incompressible page means zram is spending memory while giving you nothing. On a workload carrying already-compressed data — encrypted files, media, pre-compressed database pages — huge_pages climbs fast and the "I'm saving memory" story quietly reverses. To see the real ratio, divide orig_data_size by mem_used_total: the memory you pay for is mem_used_total. Comparing it against compr_data_size measures something else — the allocator's space efficiency.

Now zram's most critical and least discussed property: when it fills up, there is nowhere to fall back to. A full zswap pool evicts pages to the real swap behind it; zram has no such tier — a full device is just a block device at capacity, and the system walks straight towards OOM.

The antidote is backing_dev. You define a backing device and use writeback to push idle or incompressible pages to a real disk. Two setup constraints will make you fail silently: the backing device must be set before disksize, and only partitions are supported at the moment.

echo /dev/sda5 > /sys/block/zram0/backing_dev   # this first
echo 4G        > /sys/block/zram0/disksize      # then this
echo 1         > /sys/block/zram0/writeback_limit_enable
echo 4096      > /sys/block/zram0/writeback_limit
Enter fullscreen mode Exit fullscreen mode

That third line matters: writeback_limit_enable defaults to 0, and while it is 0 the writeback_limit value you wrote is meaningless. You'd think you set a budget when you didn't.

There's one more capability: recompression. In a multi-compression configuration you can define one primary and up to three secondary algorithms, then use recompress to re-compress specific page types with a stronger algorithm.

Don't skip the second half of the sizing rule either: the documentation notes that an unused zram costs about 0.1% of the disk size, so a huge zram is wasteful.

On the system side, rather than wiring this up by hand, systemd's zram-generator is cleaner: it creates the device and sets up swap at boot, with the configuration living in a single file.

zswap: a buffer you enable with one echo

The zswap side is less setup, more tuning. To turn it on at runtime:

echo 1 > /sys/module/zswap/parameters/enabled
Enter fullscreen mode Exit fullscreen mode

To make it permanent you add zswap.enabled=1 to the kernel command line; if your distribution ships with CONFIG_ZSWAP_DEFAULT_ON it's already active.

All the knobs live under /sys/module/zswap/parameters/, and here's how they look on my server:

$ grep -r . /sys/module/zswap/parameters/ | sed 's|.*parameters/||'
same_filled_pages_enabled:Y
enabled:N
shrinker_enabled:Y
max_pool_percent:20
compressor:lzo
non_same_filled_pages_enabled:Y
zpool:zbud
exclusive_loads:N
Enter fullscreen mode Exit fullscreen mode

That output is from Ubuntu 24.04 / kernel 6.8, and part of it is already history: upstream removed the zbud and z3fold allocators along with the zpool parameter — the current zswap documentation says it in one line, zsmalloc manages the compressed pool. exclusive_loads and same_filled_pages_enabled are gone from today's kernel source too. So some of these knobs may simply not exist on your machine; instead of memorising the list, read grep -r . /sys/module/zswap/parameters/.

The three that persist are these. max_pool_percent is the maximum percentage of memory the compressed pool may occupy — default 20, a fifth of your RAM. compressor is the compression algorithm; this kernel's default is LZO. accept_threshold_percent is the hysteresis threshold for when to start accepting pages again after the pool fills.

There's also shrinker_enabled, which turns on the shrinker that proactively writes cold pages out to swap. On my kernel it ships enabled — but since zswap itself is off, it has no effect.

Know from the outset that turning it off isn't symmetric with turning it on: the documentation says that when zswap is disabled at runtime it stops storing pages, but does not immediately write out or fault back the pages already in the compressed pool. To actually drain the pool you have to run swapoff on the swap device.

Rethinking swappiness

Setting up compressed swap and leaving vm.swappiness alone is a job half done. That knob answers the kernel's question of "how expensive is swap I/O relative to filesystem paging?", and the default of 60 is an estimate from the world of spinning disks.

The kernel documentation is surprisingly explicit here: the value ranges from 0 to 200, at 100 the VM treats both costs as equal, and for in-memory swap like zram or zswap, values beyond 100 can be considered. The docs even give the arithmetic: if random I/O against the swap device is on average 2x faster than filesystem I/O, swappiness should be 133 (x + 2x = 200).

Here are the values on my own server:

$ free -m | head -2
               total        used        free      shared  buff/cache   available
Mem:           96556       17525       69760        1595       11839       79031

$ swapon --show
NAME      TYPE SIZE USED PRIO
/swapfile file   8G   0B   -2

$ sysctl vm.swappiness vm.page-cluster
vm.swappiness = 10
vm.page-cluster = 3
Enter fullscreen mode Exit fullscreen mode

swappiness is 10, meaning the system treats writing to swap as almost a last resort compared with dropping page cache. That's defensible for a swapfile on disk; but if that swap moved to zram, the same 10 would mean deliberately tying your own hands.

The second line gets less attention and deserves as much: vm.page-cluster is a logarithmic value controlling how many consecutive pages are read from swap in one attempt, and it defaults to 3 — eight pages. That behaviour makes sense for spinning disks, where fetching neighbouring pages costs little extra. In compressed swap each page is decompressed individually, and seven of those eight may have been decompressed for nothing. Setting it to zero disables swap readahead entirely, which is the common recommendation for zram setups.

Changing the mechanism without changing the cost assumptions is the most common mistake in compressed swap setups — and that assumption isn't one knob, it's at least two.

Which one for which machine?

The first question in the decision tree: is there a swap area on this machine that you want to use?

If there isn't and you don't want one — a laptop whose SSD lifetime you're protecting, a diskless edge device, a VM on a tight disk quota — the answer is zram. Swap lives entirely in RAM and the disk never enters the picture.

If there is, and your actual complaint is the machine grinding to a halt whenever it swaps, the answer is zswap. Some pages stay in the RAM pool without ever reaching disk, disk traffic drops, and your existing swap area stands behind it as insurance.

Using both at once is theoretically possible, but I wouldn't recommend it — and not only because you pay CPU twice. When pages evicted from the zswap pool land in zram, cold pages stay locked in fast RAM and the LRU logic inverts. You end up with a setup doing the opposite of what you wanted, which was keeping the active working set in RAM.

So why is neither present on my 96 GB server? Because swap there is already just insurance; the machine sits at 17 GB in use and the 8 GB file swap in swapon --show reports zero usage. Compressed swap helps systems that actually fall into swap. On a system that never does, every mechanism you add raises more questions than it answers.

And there's the obvious truth nobody says out loud: neither mechanism creates memory. Compressed pages still live in RAM, they just take up less room. Under genuine memory pressure zram buys you time, it doesn't rescue you — and you buy that time with CPU cycles.

Containers, OOM and hibernation

Three more details, all of which show up during the first crisis rather than the first setup.

First, cgroup accounting — and here the two differ sharply. In both setups swap usage counts against memory.swap.max, so the expectation that "I added compressed swap, my containers can breathe now" doesn't materialise unless you loosen the limits yourself.

The real difference is in the memory held by the compressed pages. For zswap, cgroup v2 has dedicated knobs: memory.zswap.current shows that group's zswap consumption, memory.zswap.max is a hard limit, and memory.zswap.writeback set to 0 disables writing to swap devices for that group (the setting is hierarchical — disable it above and it stays disabled below). zram has no such accounting: the RAM the device holds for compressed pages is spent at host level without any record of which container it was held for. On a container-dense machine that alone is an argument for zswap if you care about isolation and pressure signals.

Second, OOM behaviour. Compressed swap delays memory exhaustion, it doesn't cancel it — and while it delays, the system burns CPU. If you want to catch memory pressure early and evict in a controlled way, the answer isn't compression but a pressure-based policy; the approach in my systemd-oomd cgroup v2 memory pressure runbook is complementary here. Compressed swap buys you time; policy decides what you do with it.

Third, hibernation. The hibernation image has to be written to a persistent device, and a zram device living in RAM vanishes the moment power goes. If you set up zram on a laptop and still expect hibernate to work, you need a separate swap area on disk.

These three are different faces of what I lived through in my first OOM and swap storm: in memory management, turning a single knob rarely settles anything.

Don't decide without measuring

The sneakiest thing about compressed swap is that whether it's working isn't obvious. After setup, the places to look:

  • awk '{print $1/$3}' /sys/block/zram0/mm_stat → the real compression ratio (orig_data_size / mem_used_total). The field order in that file is orig_data_size compr_data_size mem_used_total mem_limit mem_used_max same_pages pages_compacted huge_pages huge_pages_since. Track incompressible workloads with huge_pages_since, which counts since setup, rather than the instantaneous huge_pages.
  • grep -r . /sys/kernel/debug/zswap/ → this is where zswap is measured; the documentation notes a debugfs interface providing statistics about pool size, the number of pages stored, and counters for why pages were rejected.
  • swapon --show → which swap area is actually being used, and at what priority.
  • cat /proc/pressure/memory → did memory pressure really drop, or did it just move?
  • the si/so columns of vmstat 1 → the volume of page in/out.

As a checklist: for zram, confirm the module package is installed; don't make disksize larger than twice your RAM; reconsider swappiness for compressed swap (persuading the kernel to be more willing can make sense when swap doesn't touch disk); for zswap, tune max_pool_percent to your workload's real compression ratio; and on both sides, do a reboot drill before making the change permanent.

Rethinking swap

Putting these two mechanisms side by side, what strikes me isn't their technical difference but that both are answers to the same question: when memory runs short, what gets sacrificed?

Classic swap sacrifices the disk — latency explodes but the system stays up. zram and zswap sacrifice CPU instead, and in return keep latency far lower. Which is right depends on which resource is abundant in your system.

So the question for your own setup is this: is your bottleneck really memory, or the disk latency that appears the moment memory gets tight? If it's the second, the fix may not be buying more RAM — sometimes an echo 1 is enough.

Official Sources

Top comments (0)