GC Roots and Space Reclamation

Updated

September 12, 2026

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 | head

You should recognise result, profiles, and /run/current-system (on NixOS).

Case 3: Safe collect versus drop generations

nix-collect-garbage

Keeps every NixOS / Home Manager generation. Safe on a Monday morning.

nix-collect-garbage --delete-older-than 14d

Drops 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 -d

Drops 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-pager

That 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 7d

or 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-garbage is the only deletion tool.
  • --delete-older-than 14d on workstations; -d when disk is the incident.
  • Delete leftover result symlinks you do not need.
  • On NixOS, enable automatic GC with a retention window — not rm.

Try this

  1. 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.
  2. Count generations: sudo nix-env --list-generations --profile /nix/var/nix/profiles/system (NixOS) or nix-env --list-generations (user).
  3. Dry-run: nix-collect-garbage --dry-run and read what would go. Then run without --dry-run only if you agree.
  4. On a NixOS lab VM, add nix.gc.automatic = true; and nix.gc.dates = "weekly";, switch, and systemctl status nix-gc.timer.