Swap, ZRAM, Zswap and Hibernate on NixOS

If you want to use hibernate you need swap space and that complicates your choice around ZRAM and ZSWAP. I made several wrong choices before I found the correct one.
I pulled this out of my non-obvious nixos config piece into its own post.

Oh no, I'm going to talk about swap, zram, zswap...

Whether to use swap is a perennial fight online. The conversation is often muddled when people conflate recommendations for servers with recommendations for workstations. In my case, I'm going to focus on desktops and laptops.

I use swap because I like suspend-then-hibernate, or "Standby, then hibernate" in the KDE settings:
![[Pasted image 20260917120542.png]]

This will first ACPI S3 suspend to RAM, then after a delay go into S4 Hibernate. I think this strikes a nice balance: if I step away for a bit I don't have to wait for things to start up, but I don't have to worry if I forget to fully power off my machine.

To use hibernate you need swap to save your system state to disk.


Let's add even more fuel to the fire

There are also arguments over swap files vs swap partitions. I mostly want to note that swap files can complicate hibernate. That is because when you resume from hibernate initramfs needs to know the location of the swap space before the file systems are mounted. On a UEFI system the details are saved into an EFI variable when hibernation starts.

I set up my desktop with a simple swap partition:

  swapDevices = [
    {device = "/dev/disk/by-uuid/f2fc399a-9703-450b-88df-5671b776fc71";}
  ];

I set up my laptop with LUKS encryption and a swap file:

  fileSystems."/.swapvol" = {
    device = "/dev/disk/by-uuid/33682d1d-87c4-4166-8d3b-66d900387e42";
    fsType = "btrfs";
    options = ["subvol=swap"];
  };
  
  ...
    swapDevices = [
    {
      device = "/.swapvol/swapfile";
      size = 32 * 1024;
    }
  ];

This is based on the disko luks-btrfs-subvolumes template.

The good news is that for a while now NixOS understands the nuances of hibernate on swap files and can handle figuring out the offsets.


The config I currently use

If I'm going to use swap I want to make sure I use it right. The piece In Defence of Swap and the followup Debunking zswap and zram myths provide a lot of important context. As a starting point, Chris Down recommends using zswap with disk-backed swap.

So my current configuration is simply:

  boot.zswap.enable = true;
  boot.kernel.sysctl."vm.swappiness" = 100;

Yes, literally just that. I like the fact that the config is not long and complicated. Just because it's a couple of lines does not make it any less important.

Why that particular swappiness value? Swappiness controls the relative cost of swapping and filesystem paging so it will depend on your particular hardware. Chris suggests that a swappiness of 100 works well on systems with SSDs, but that it's "non-trivial to tune this value based on instinct alone" and you should test different values. The kernel docs agree and say you can even go higher on SSDs.

However, I haven't found clear instructions anywhere on how to test and compare different swappiness values, especially for workstation usage. If you have any recommendations, I would appreciate hearing about them. For now, 100 is serving me well with my SSDs.


The mistakes I made along the way

Before I read Chris's second article I was using zramSwap.enable since I thought this was what he advocated. However, I had that wrong and it was actually the opposite. I didn't realize this until April of this year when boot.zswap was added. So to clarify, zramSwap is what is normally just referred to as zram. It is likely not what you want when using disk based swap. In my case I wanted hibernation so this was the wrong choice.

After my realization I removed zramSwap.enable and started using boot.zswap:

  boot.initrd.systemd.enable = true;
  boot.zswap = {
    enable = true;
    compressor = "lz4";
  };
  boot.kernel.sysctl."vm.swappiness" = 100;

You'll notice that the compression algorithm for zswap can be controlled through boot.zswap.compressor. The Linux kernel defaults to lzo since it strikes a good balance of speed and compression. Whereas NixOS defaults to zstd with the reasoning that it has the best compression ratio and is good for Nix builds.

Up until recently I was running lz4 which has the fastest compression and lowest latency. I made the decision after reading around the ArchWiki which primarily uses that as the compressor. Realistically though compression speed is often not the limit when you are dealing with memory spilling over to disk. A better compression ratio lets you fit more pages into the compressed pool in memory, so you use the disk less.

So now I use the NixOS default of zstd. Also, when I added that config lz4 required boot.initrd.systemd.enable. That setting now defaults to true so I can drop that line as well. That leaves us with just two lines of config.

I write about a variety of projects, events and thoughts, but they all share one aspect: I learned something and want to share my experience with others.

No spam, no sharing to third party. Only you and me.

Member discussion