When memory tightens, cold pages would go to a slow disk. zram keeps them in compressed RAM instead, trading a little CPU for much more headroom.
A compressed block device in memory
zram is a block device that lives in RAM and stores compressed data instead of talking to a real disk. When the kernel swaps a page into it, the page is compressed and kept in memory, so the memory gain comes from shrinking the pages rather than moving them to slow storage. Because the device is memory-backed, it answers reads and writes far faster than a disk-backed swap, but the space it provides is limited by how compressible the pages are and by how much memory you are willing to trade for the swap.
Compression is the space win
Compression is the space win, and it is the reason a small box can appear to hold more memory than it physically has. Whether a page pays off depends on its content: text, zeroed pages and structured data compress well, while already-compressed blobs such as media or archives barely shrink. The honest engineering view is that zram turns spare CPU cycles into apparent memory, so the trade is worthwhile when the workload leans compressible and the extra latency of a real disk swap is exactly what you want to avoid.
It suits gove boxes and phones
The fit shines on hosts with little memory and modest I/O, such as a small VPS, a container host, or a compact device like a phone or an embedded box. It is less obviously right for a server that already has plenty of RAM or that swaps into genuinely cold storage anyway, because a real disk bought with the same spend can hold far more. The practical check is to watch the compression ratio and swap usage, decide whether swapping beats killing the process, and pick zram when resident data stays small and compressible enough to make the CPU cost a fair trade.
When it breaks
A ratio that never arrives is the commonest failure. Photos, video, encrypted buffers, and archives barely shrink, so mm_stat shows orig_data_size just above compr_data_size while swap still fills. The pool and its metadata consume RAM yet add almost no headroom, so the OOM killer fires nearly as if zram were absent.
The wrong compressor fails through CPU, not capacity. A heavy algorithm on a small VPS or old handset makes every swap-out burn cycles, so kswapd spins, system CPU climbs with pressure, and tail latency collapses before swap fills. Prefer fast choices such as lzo-rle, lz4, or low-level zstd on interactive hosts, and change only after measuring under load.
Running zswap with zram compresses twice for no gain. zswap holds its own compressed pool, then hands already-shrunk pages to zram, which finds little left to save. CPU burns on both paths while swap looks used with small effective capacity. Use one layer: zram as swap, or zswap before disk, never both.
A hard-coded disksize goes stale after any resize. A unit that pins 2 GiB still offers 2 GiB after the instance grows to 32 GiB, wasting headroom; copied to a tiny edge box, the same file reserves half of RAM and hastens OOM. The size is fixed at mkswap time, so express it as a fraction of RAM at boot.
Check it quickly
Save the following as zram_ratio.py and run python3 zram_ratio.py. It uses only the standard library, opens /sys/block/zram*/mm_stat read-only, and needs no privileges. Without those files it prints SKIP and demonstrates the same ratio math on a small example instead.
#!/usr/bin/env python3
"""Read-only zram compression-ratio check (stdlib only)."""
import glob
from pathlib import Path
def parse_mm_stat(text):
parts = text.split()
if len(parts) < 2:
raise ValueError("mm_stat needs at least orig and compr fields")
orig = int(parts[0])
compr = int(parts[1])
rest = [int(x) for x in parts[2:]]
return orig, compr, rest
def report(device, orig, compr):
if compr <= 0:
print("%s: orig=%d compr=%d ratio=n/a (empty)" % (device, orig, compr))
return
ratio = orig / compr
saved = 100.0 * (1.0 - compr / orig) if orig > 0 else 0.0
print("%s: orig=%d compr=%d ratio=%.2f saved=%.1f%%" % (device, orig, compr, ratio, saved))
def main():
paths = sorted(glob.glob("/sys/block/zram*/mm_stat"))
if not paths:
print("SKIP: no /sys/block/zram*/mm_stat found (zram absent? non-Linux?)")
orig, compr = 300 * 1024 * 1024, 100 * 1024 * 1024
print("worked example with the same formula:")
report("example/zram0", orig, compr)
print("PASS: ratio math works; on a zram host the live lines above replace the example")
return
for path in paths:
try:
text = Path(path).read_text(encoding="utf-8")
except OSError as exc:
print("SKIP: unreadable %s (%s)" % (path, exc))
continue
try:
orig, compr, _ = parse_mm_stat(text)
except (ValueError, IndexError) as exc:
print("SKIP: unparsable %s (%s)" % (path, exc))
continue
device = path.split("/mm_stat")[0]
report(device, orig, compr)
print("PASS: live mm_stat accounting shown; falling ratio plus full pool means resize or recompress")
if __name__ == "__main__":
main()
On a zram host it prints one line per device with original bytes, compressed bytes, the compression ratio, and memory used, so falling ratios and a full pool are visible before OOM. On macOS or a container without zram* it prints SKIP plus the worked example. The script passed on macOS without /sys. It checks accounting and arithmetic only, not which algorithm or disksize is correct, so confirm those in the unit files before tuning.
Further reading
Sources
- kernel.org: zram block device, the authoritative zram documentation, checked 2026-11-23.
- Wikipedia: zram, what zram is and how it works, checked 2026-11-23.
- ArchWiki: Zram, practical setup and tuning, checked 2026-11-23.