GC Roots and Space Reclamation
GC Roots and Space Reclamation
Nix never overwrites a path, so /nix/store grows until something says “this is still needed.” The boring default is: roots keep closures alive; nix-collect-garbage deletes everything that is not reachable from a root; -d is how you drop old generations on purpose.
Mental model
A GC root is a pointer outside the store (or a special symlink inside gcroots/) that names a store path. The collector starts from roots and walks --referrers in reverse: anything unreachable is garbage.
Common roots:
| Root | Typical path | Keeps alive |
|---|---|---|
| Current NixOS system | /run/current-system and /nix/var/nix/profiles/system |
The running OS plus old generations |
| User profile | ~/.nix-profile |
nix profile / Home Manager user env |
| Home Manager generations | ~/.local/state/nix/profiles/home-manager |
Previous home-manager switch results |
result symlink |
./result → registered in gcroots/auto |
The last nix-build in that directory |
| direnv | .direnv/flake-profile |
The cached devShell |
| Channels / flake registry | ~/.nix-defexpr, registries |
Pin data, not usually huge |
roots → profiles / result / current-system
│
▼
walk references
│
▼
keep those paths; delete the rest
nix-collect-garbage without -d deletes orphans (paths with no root) but keeps generations. -d deletes old generations first, then orphans. That is the difference between “I freed 80 MB” and “I freed 12 GB.”
Worked examples
Case 1: See the roots
ls /nix/var/nix/gcroots
ls -l /nix/var/nix/gcroots/auto 2>/dev/null | head
nix-store --gc --print-roots 2>/dev/null | headYou should recognise result, profiles, and /run/current-system (on NixOS).
Case 2: A result symlink is a root
nix build nixpkgs#hello --out-link /tmp/hello-result
nix-store --gc --print-roots | grep hello-resultOutput:
/tmp/hello-result -> /nix/store/…-hello-2.12.1
Delete the symlink, then collect:
rm /tmp/hello-result
nix-collect-garbageOutput:
finding garbage collector roots...
deleting garbage...
… store paths deleted, … MiB freed
If hello is still in your profile, it will not be deleted. Only the extra root is gone.
Case 3: Safe collect versus drop generations
nix-collect-garbageKeeps every NixOS / Home Manager generation. Safe on a Monday morning.
nix-collect-garbage --delete-older-than 14dDrops generations older than fourteen days, then deletes newly orphaned paths. Prefer this over -d on a machine you might still want to roll back.
sudo nix-collect-garbage -dDrops all old generations of all profiles. The running system and the current user profile stay. Rollback targets vanish. Use it when disk is the incident.
On NixOS, save as gc.nix:
# gc.nix
{
nix.gc.automatic = true;
nix.gc.dates = "weekly";
nix.gc.options = "--delete-older-than 14d";
nix.settings.auto-optimise-store = true;
boot.loader.systemd-boot.configurationLimit = 8;
}sudo nixos-rebuild switch
systemctl status nix-gc.timer --no-pagerThat timer is the boring weekly. -d remains a human incident command, not the timer.
Case 4: Why CI disks fill up
CI runners that nix build without deleting result, without a root that expires, and without GC between jobs accumulate every closure they ever substituted. The boring CI pattern:
nix build .#desk-api
nix-collect-garbage --delete-older-than 7dor a persistent runner with auto-optimise-store and a nightly GC. Ephemeral GitHub runners are destroyed anyway; self-hosted runners are not.
Case 5: Do not GC the system you are running
readlink /run/current-system
sudo nix-collect-garbage
ls /run/current-system/run/current-system is a root. The collector will not delete the running closure. It will delete old generations if you passed -d. After -d, nixos-rebuild switch --rollback has nowhere to go except whatever generations you still have.
The trap
The trap is sudo rm -rf /nix/store/* because df is 100%. That races the daemon, corrupts db.sqlite, and can unboot NixOS. Always GC.
The second trap is running -d on a laptop the day before a risky upgrade, then needing the previous generation at 2 a.m. Keep two weeks of generations on machines that boot humans.
The third trap is leaving a result in $HOME forever. It pins an old clang forever. find ~ -name result -type l once a quarter.
The boring rule
- Roots keep closures. No root, GC is allowed to delete.
nix-collect-garbageis the only deletion tool.--delete-older-than 14don workstations;-dwhen disk is the incident.- Delete leftover
resultsymlinks you do not need. - On NixOS, enable automatic GC with a retention window — not
rm.
Try this
nix build nixpkgs#cowsay --out-link /tmp/cow && nix-store --gc --print-roots | grep /tmp/cow. Remove the link and GC. Confirm the cowsay path is gone unless something else roots it.- Count generations:
sudo nix-env --list-generations --profile /nix/var/nix/profiles/system(NixOS) ornix-env --list-generations(user). - Dry-run:
nix-collect-garbage --dry-runand read what would go. Then run without--dry-runonly if you agree. - On a NixOS lab VM, add
nix.gc.automatic = true;andnix.gc.dates = "weekly";, switch, andsystemctl status nix-gc.timer.