After almost 2 and half months of using NixOS, I had my first freeze that needed to use the power button to reset. A Rust build was running, memory reached about 96%, and the desktop completely froze.
The workstation has 32 GB of RAM. But, It had no swap at all.
Check what happened first
After restarting, I checked swap and the previous boot’s logs:
swapon --showfree -hjournalctl -b -1 --no-pager | grep -Ei 'out of memory|oom|memory pressure|killed process'Here is an example output from journalctl:
Nov 09 08:52:31 nixos kernel: OOM killer disabled.Nov 09 08:52:31 nixos kernel: OOM killer enabled.Nov 09 09:24:32 nixos systemd-journald[576]: Under memory pressure, flushing caches.Nov 09 09:24:35 nixos systemd-journald[576]: Under memory pressure, flushing caches.The journal showed memory pressure just before the restart. I couldn’t find a recorded OOM kill because I used physical power button to restart.
So I can say the machine was under memory pressure, but I can’t prove the kernel’s out-of-memory killer caused the freeze. Even though I am pretty certain about it.
Nix also allowed 20 simultaneous builds, with each build allowed to use all CPU cores. That was more aggressive than I wanted on a workstation I was still using.
Add swap and reduce build concurrency
I chose 32 GiB of disk swap, plus zram at 25% of RAM. Zram stores compressed pages in RAM (opens in a new tab). Its higher priority makes Linux use it before disk swap.
My root filesystem is single-device Btrfs. I added the following inside the existing attribute set in configuration.nix:
swapDevices = [ { device = "/swap/swapfile"; size = 32768; # MiB priority = 10;} ];
zramSwap = { enable = true; algorithm = "zstd"; memoryPercent = 25; priority = 100;};
nix.settings.max-jobs = 2;nix.settings.cores = 4;
systemd.services.mkswap-swap-swapfile.preStart = '' if [ ! -e /swap ]; then btrfs subvolume create /swap fi btrfs subvolume show /swap >/dev/null chmod 0700 /swap'';The pre-start hook creates a separate /swap subvolume before NixOS creates the swapfile. It assumes /swap is unused or already a Btrfs subvolume. An ordinary directory there will fail the check. This hook is specific to my Btrfs layout.
Keeping swap in its own subvolume avoids including it in parent snapshots. Active swapfiles still affect some Btrfs maintenance operations, including balance and scrub. Read the Btrfs swapfile restrictions (opens in a new tab) before copying this setup.
Watch the first rebuild
The catch is timing: swap becomes active after the configuration builds. Adding it won’t protect the build needed to activate it.
For Codex Rust build, I changed it to the package source’s signed binary cache before retrying. Check that your rebuild downloads the package instead of starting the same large compilation again.
Keep your usual rebuild target and pass --max-jobs 2 --cores 4 to the first nixos-rebuild test so it uses the lower limits before they’re activated.
I tested the configuration, then switched it permanently. After activation, swapon --show reported:
NAME TYPE SIZE USED PRIO/dev/zram0 partition 7.8G 0B 100/swap/swapfile file 32G 0B 10Zero usage is fine when RAM is available. I haven’t repeated the heavy build or checked swap persistence after a normal reboot yet.
If you undo this later, removing the declaration leaves the swapfile on disk. Plan its deactivation and deletion when there’s enough free RAM. Turning off heavily used swap during memory pressure can exhaust RAM again.
Comments