Nixos Store Analysis and Cleanup
FleetingNixOS keeps every store path alive as long as something references it — a
generation, a GC root, a result symlink, even a running process. Over time the
/nix/store grows silently. This note walks through understanding what takes
space, finding what can be freed, and cleaning it up.
How big is my store?
Start here. This gives the total disk usage of the nix store.
du -sh /nix/store /nix/var
35G /nix/store
77M /nix/var
Almost everything lives in /nix/store. The /nix/var overhead is negligible.
Understanding what takes space
The store is shared: many packages depend on the same libraries, so closure sizes overlap. A 7GB system closure + a 7GB home-manager closure might only use 10GB on disk because glibc, Qt, etc. are counted once.
List all GC roots
Everything alive in the store is reachable from a GC root. This shows what’s keeping paths alive:
nix-store --gc --print-roots 2>&1 | grep -v '/proc/'
/home/sam/.cache/nix/flake-registry.json -> /nix/store/pnaz66ik1q8alabvzm895x2i66kcnvbx-flake-registry.json
/home/sam/.local/state/home-manager/gcroots/current-home -> /nix/store/fl4p4ygwbsfrgd52xp81kynqa1kfha7b-home-manager-generation
/home/sam/.local/state/nix/profiles/home-manager-105-link -> /nix/store/60z853dchkj350s3vlq1d7pqivr8g07v-home-manager-generation
/home/sam/.local/state/nix/profiles/home-manager-106-link -> /nix/store/xq3pkycr0nc1p4gnxdy7042dbw5nfnhz-home-manager-generation
/home/sam/.local/state/nix/profiles/home-manager-107-link -> /nix/store/fl4p4ygwbsfrgd52xp81kynqa1kfha7b-home-manager-generation
/home/sam/.local/state/nix/profiles/profile-434-link -> /nix/store/pxz6z7ngmclnggc8girwyy244pq2d14v-profile
/home/sam/prog/keyban/dap/.devbox/nix/profile/default-5-link -> /nix/store/4aga4hk2cangqgnla1xjrwhm05lm3cqn-profile
/nix/var/nix/profiles/per-user/root/channels-1-link -> /nix/store/3rx5srpc6jlmhaym7qzj80123szcvqnj-nixos-25.11.6820.c581273b8d5b
/nix/var/nix/profiles/system-83-link -> /nix/store/25v0g8cfddwqf6f73acadangijf5jax1-nixos-system-konix-26.05.20260303.8c809a1
/nix/var/nix/profiles/system-84-link -> /nix/store/14x04k80gdygzh03laf7qgi1laqlsgs7-nixos-system-konix-26.05.20260303.8c809a1
/run/booted-system -> /nix/store/14x04k80gdygzh03laf7qgi1laqlsgs7-nixos-system-konix-26.05.20260303.8c809a1
/run/current-system -> /nix/store/14x04k80gdygzh03laf7qgi1laqlsgs7-nixos-system-konix-26.05.20260303.8c809a1
The /proc/ entries are from running processes — they go away on reboot or
when the process exits. The interesting roots are:
~/.local/state/nix/profiles/profile-*-link— user profile generations fromnix profile install. Often the biggest root because it accumulates every package you’ve ever installed. This profile is populated on-demand by the hardliases system (~/prog/devel/bin/hardliases/): each hardlias is a wrapper script that callsnix profile addon first run. So the profile grows over time as you use more commands. Cleaning old generations withnix profile wipe-historyis safe — the hardliases will reinstall on next use. Removing a package from the profile entirely is also safe for the same reason, but it will trigger a rebuild on next invocation./nix/var/nix/profiles/system-*-link— NixOS system generations. Each one is a full bootable system closure. Clean withsudo nix-collect-garbage --delete-older-than 7d.resultsymlinks in project directories — leftovernix buildoutputs. Each one registers a GC root in/nix/var/nix/gcroots/auto/that persists even if the symlink is deleted. Can hold large closures alive.~/.local/state/nix/profiles/home-manager-*-link— Home Manager generations. Old ones keep their full closure alive. Clean withhome-manager expire-generations./tmp/symlinks — same asresult, but in/tmp. The symlinks themselves disappear on reboot, but the GC root entries persist.nix-collect-garbagedetects and removes these stale roots automatically..devbox/nix/profile/— devbox project environments. Each project with adevbox.jsongets its own nix profile./nix/var/nix/profiles/per-user/root/channels-*-link— the root user’s channel (e.g. the nixpkgs snapshot used bysudo nixos-rebuild). Usually a few hundred MB. Cleaned bysudo nix-collect-garbage -d.~/.local/state/home-manager/gcroots/current-home— symlink to the active Home Manager generation. Managed by Home Manager, don’t touch it./run/current-system— points to the currently active system generation (may differ frombooted-systemafternixos-rebuild switchwithout rebooting). Managed by NixOS, don’t touch it./run/booted-system— points to the system generation that was booted. Set at boot, read-only. Goes away on reboot.~/.cache/nix/flake-registry.json— cached copy of the global flake registry that maps shorthand names (likenixpkgs) to URLs. Tiny, harmless.
Closure sizes per root
A closure is a package plus everything it depends on. This shows how big each
major root is (with overlap — they don’t sum to the total). In particular,
the user profile includes home-manager-path as one of its entries, so its
closure contains the entire Home Manager closure plus all other packages
installed via nix profile install.
echo "=== NixOS system ==="
for p in /nix/var/nix/profiles/system-*-link; do
size=$(nix path-info --closure-size "$p" 2>/dev/null | awk '{printf "%.1f GB", $2/1024/1024/1024}')
echo " $(basename $p) -> $size"
done
echo ""
echo "=== Home Manager ==="
for p in ~/.local/state/nix/profiles/home-manager-*-link; do
size=$(nix path-info --closure-size "$p" 2>/dev/null | awk '{printf "%.1f GB", $2/1024/1024/1024}')
echo " $(basename $p) -> $size"
done
echo ""
echo "=== User profile ==="
if [ -e ~/.local/state/nix/profiles/profile ]; then
size=$(nix path-info --closure-size ~/.local/state/nix/profiles/profile 2>/dev/null | awk '{printf "%.1f GB", $2/1024/1024/1024}')
echo " profile -> $size"
fi
=== NixOS system ===
system-83-link -> 6.8 GB
system-84-link -> 6.8 GB
=== Home Manager ===
home-manager-105-link -> 7.3 GB
home-manager-106-link -> 7.3 GB
home-manager-107-link -> 7.3 GB
=== User profile ===
profile -> 29.3 GB
Zoom in
Nixos
These are the individual store paths (not closures) that take the most space inside the NixOS system generation. This helps understand what the system itself pulls in — firmware, mesa, kernel modules, docker, etc.
nix path-info --size -r /nix/var/nix/profiles/system | \
awk '{sz=$2/1024/1024; if (sz>=1024) printf "%.1f GB %s\n", sz/1024, $1; else printf "%.0f MB %s\n", sz, $1}' | sort -t' ' -k1 -rn | head -100
734 MB /nix/store/g8fv0vl1zq3nm823jm93n95y3dafp7b1-linux-firmware-20260221-zstd
645 MB /nix/store/y0n2rsgi3lfdsgvh2rdkglnb2c07a4gs-source
540 MB /nix/store/s6v0k4cvllyp5cy03ngxz0snj7z23ljm-llvm-21.1.8-lib
528 MB /nix/store/nax6synifbl6syx1l2sxjrf1xh1qprqb-llvm-21.1.8-lib
260 MB /nix/store/7bcamlsr1y22gsi3br0dvw0slw5k7qqz-mesa-26.0.1
252 MB /nix/store/6rcij4a3pcjm94r0k0i3vil6qyhaamdy-mesa-26.0.1
222 MB /nix/store/0qlmqvyckf8zyx03n00azgznnkzgi5xw-glibc-locales-2.42-51
187 MB /nix/store/affmc6lhad8f6q3iaa3iydcdjwr8lwgp-source
165 MB /nix/store/4z51xyah9h8h3al1wclvgy6cb04vq0vl-qtdeclarative-6.10.2
133 MB /nix/store/6wm58zyrnz70240xywc0l9ml18rf03qg-linux-6.18.15-modules
107 MB /nix/store/m1fw8l8y9ycxh5dzispbb7cwl6rra14l-python3-3.13.12
99 MB /nix/store/iarcrhvmxrk4hfqqhy9di2kagb2rjwji-moby-29.2.1
98 MB /nix/store/9g44xzv8bxq3n9pg3h7wjjcbqg70gy4l-docker-containerd-29.2.1
86 MB /nix/store/rkh0rc2nhdy7iian5vdhbsb8fmy0x21x-docker-buildx-0.31.1
83 MB /nix/store/w4q31b93w262q2b75ri3jc7m3xd4i31h-qtbase-6.10.2
74 MB /nix/store/z2hrkj6a6vcbyf90924x2chhmlgwqmpq-python3-3.13.12
61 MB /nix/store/crabwpx7qyz7rysfdvsfg4dclk811q9d-xscreensaver-6.13
61 MB /nix/store/1m0z4ixir10fg7ba7g131js04bxgv265-cmake-4.1.2
60 MB /nix/store/73f5hxj105ny4q0phlv616lwic4sv6n4-flite-2.2-lib
59 MB /nix/store/r042cd7600rhl130idnmwxqyjc9126s4-systemd-259
56 MB /nix/store/wxyn8d3m8g4fnn6xazinjwhzhzdg6wib-systemd-259
55 MB /nix/store/4pfrhja4j3j5g4iwssn9w43sgihdf4wl-perl-5.42.0
51 MB /nix/store/swibh67af55w3pxsjlksyxfihamf9i2z-git-minimal-2.53.0
49 MB /nix/store/sp3pq0in1ca25hyr0ffigvx2g2dhqs91-noto-fonts-2026.02.01
49 MB /nix/store/fy54dwdr2wxf1phq3sgvrdyqdimzb48p-ghostscript-with-X-10.06.0
41 MB /nix/store/q43d6pbb9qxyhj6hq4id5mdg6gfrz6gv-gtk+3-3.24.51
38 MB /nix/store/wcmcai1w6ww72hflq95k61fsx7kz0d2q-icu4c-76.1
38 MB /nix/store/3zlq53qsimf0y525qxycjha8px0q7ygj-icu4c-76.1
34 MB /nix/store/9jk9p6679rz3lsc0mdnyf8h8yq4y8cc6-docker-29.2.1
33 MB /nix/store/ip1zll830af00llffx8s711dhvr51vmq-ffmpeg-headless-8.0.1-lib
33 MB /nix/store/8x2msxrm9yb8ycz9pi1pi8wz5zs9gy64-ffmpeg-8.0.1-lib
32 MB /nix/store/wd2988s9rndikj1l849l350zyv7lzz6h-freepats-20060219
29 MB /nix/store/zls8yqy7drgh35gp3nrm27ljw89n6d8p-pipewire-1.4.10-doc
29 MB /nix/store/r5ifa1m0sqa6ny76jgglds7gr540837m-gtk4-4.20.3
29 MB /nix/store/n2115sg4klccw8a2rq50ndffi7sizmrg-speech-dispatcher-0.12.1
29 MB /nix/store/l0l2ll1lmylczj1ihqn351af2kyp5x19-glibc-2.42-51
29 MB /nix/store/alal0jcsb5gyg9k6x979743k2d0lxamh-ffmpeg-headless-8.0.1-lib
28 MB /nix/store/9l26qshd1kimgay1l8mqab8yb50cfdd7-glibc-2.42-51
27 MB /nix/store/q2d8xphgqlvvdzlnb3pciryjxljq6c09-docker-compose-5.0.2
27 MB /nix/store/pik5qhm6hzigjffh1aqarjc9gd351281-nixos-manual-html
25 MB /nix/store/1whg68gwyb9q9jjasqvjigqs3jma9q7y-rootlesskit-2.3.6
24 MB /nix/store/drba08mg0sbqrsi8phkjbywkj3kn0fac-x265-4.1
23 MB /nix/store/7cqhfagsrlp3h6pv273ihfd1k694gsa4-iso-codes-4.20.1
23 MB /nix/store/5x3b9ijj1bb2fh8zqskl91rn41lh5l0x-iso-codes-4.20.1
22 MB /nix/store/8rw5ajghj5pjqjcx0armbbnbq3cdn270-linux-6.18.15
21 MB /nix/store/nj9xirbbrcj2mfvb6brd9l1hrygyrkiw-nix-manual-2.31.3
20 MB /nix/store/3nb4k9pzmwxq4gxqprhnb37np7yqjwqc-networkmanager-1.56.0
19 MB /nix/store/md592gfars4m9madyrpj9yrq5jhckgjf-gettext-0.26
19 MB /nix/store/g5kvz35643fp9yhxnkwcnwkkrhyj6d71-systemd-minimal-259
19 MB /nix/store/cg2s5xhv346wyy9lfxk9m53g9rra9xhm-espeak-ng-1.52.0.1-unstable-2025-09-09
19 MB /nix/store/5q8y4gqcrp6dnwxkkcqsl4xp1nd39q0b-networkmanager-1.56.0
19 MB /nix/store/356k9ng1i14ssg5pizc66lhywr7rdbsv-systemd-minimal-259
19 MB /nix/store/1saivc2rj6rn1ykvm4rdzlddg41a2wqa-steam-unwrapped-1.0.0.85
17 MB /nix/store/vpq2b3g9rydknxbyxwrdz45vz6kk3fqv-unifont-16.0.03
16 MB /nix/store/x6adcywj0fgd4air162d1pphzh8cjvqn-gst-plugins-bad-1.26.5
16 MB /nix/store/fcqhn9m2sih0rmhw1j1ffxrkl8vw7248-pipewire-1.4.10
15 MB /nix/store/rvvxz8y9smssjbh8x9kk0zdy2iv8ywy5-glib-2.86.3
15 MB /nix/store/plsmwx39r785b1dyblqp4lv81gbdpy4c-qttranslations-6.10.2
15 MB /nix/store/6r0krhrml6hz987s40kbqczq5923i7ix-librsvg-2.61.3
15 MB /nix/store/1qm74vf93ik1xjrr9kl6qvjrklljlcqh-glib-2.86.3
15 MB /nix/store/1aqyzz6pgi49sb8lngypa9ddh5gv993q-pipewire-1.4.10
14 MB /nix/store/y6mlyr480fgdd5bgpcfwwraa6vivhmwj-gnupg-2.4.9
14 MB /nix/store/wrwzn9h21jzg24dilb15wsh0010idkk6-boost-1.89.0
14 MB /nix/store/m04yk1kq9qqigl8c260qrp3asdz1wnw1-modemmanager-1.24.2
14 MB /nix/store/6fsxn74jfpd2ffk454qsjyk8jzcqzm4w-texinfo-interactive-7.2
13 MB /nix/store/q3f55hdd85088xx3sx98x9vmac13gdjk-libajantv2-17.5.0
13 MB /nix/store/h7whg7naplaib6ykafbqfybl1w748x64-docker-runc-29.2.1
13 MB /nix/store/db6sbfmg9zb4g1v77kwpd4waq0ij55nd-util-linux-2.41.3-lib
13 MB /nix/store/8aq1xr8m40x2mrlnmm06mzzjmjv9fvx0-hwdb.bin
13 MB /nix/store/7kxx839gkx9vdyk9wfzq14l66bhdmjsd-wireplumber-0.5.13-doc
13 MB /nix/store/2sfycqdy94jdin4qq04yws1wgq10yz6l-mbedtls-3.6.5
13 MB /nix/store/15hqn31hgllprki8p6l67whix262nn3d-modemmanager-1.24.2
12 MB /nix/store/qsh3yma0sycqh4fcbi48yxv5b1ja7sn9-initrd-linux-6.18.15
12 MB /nix/store/m80scad7b9qzkc0cdk2i4d640ypphbmd-coreutils-full-9.10
12 MB /nix/store/a87fr6776d06pa1m2lx393swn0l4sgla-mbedtls-3.6.5
11 MB /nix/store/aji4a8j115inll9j57w91489ycxaq1b7-sof-firmware-2025.05.1-zstd
11 MB /nix/store/a86d34l78hd1cm0dg3fw09c0qqr245dw-xorg-server-21.1.21
10 MB /nix/store/ypxd9dldr1j8lbg7a5ws42gqa2kizgyh-bluez-5.84
10 MB /nix/store/rg4a85a1rnnagbvapkrp3xfdvg7m140w-noto-fonts-color-emoji-2.051
10 MB /nix/store/p8zzznrm6q1mrlwy6i5z3zr4f87cdc8w-hwdata-0.404
10 MB /nix/store/m3b3fnv33pzhhbadwg3qdqpcjvlg83jn-hwdata-0.404
10 MB /nix/store/ihpdbhy4rfxaixiamyb588zfc3vj19al-gcc-15.2.0-lib
10 MB /nix/store/hndbrrk1gggj5lkd8d9x7q0s7gk94z63-steam-1.0.0.85-fhsenv-rootfs
10 MB /nix/store/h5c0q8a7c61zqgnhqq3psk3g76ajpldh-steam-run-1.0.0.85-fhsenv-rootfs
10 MB /nix/store/7l9d49jd3c3a9hyqx70frrn616n3il4i-xkeyboard-config-2.46
10 MB /nix/store/6cms9f4w6pxp7gkxf464zg031w27pnag-freefont-ttf-20120503
10 MB /nix/store/4g07cwy57p5214szx7fmlkijpb6p6r47-bluez-5.84
10 MB /nix/store/2r1nbfavp94gl5q660vg3x0s4a5vyl6s-libjxl-0.11.2
9 MB /nix/store/vlhrk50xg0lmivk049q8yygbsm86l6m0-openssh-10.2p1
9 MB /nix/store/q8zb95f4m2aahbdi6jl7wh3cjwc4p855-cups-2.4.16
9 MB /nix/store/p96a7p297gmia8zcy4i72qd45wzw8lh6-openssl-3.6.1
9 MB /nix/store/kgf6zn39f6mxg0l3hjx29rpg7a0y3viv-gst-plugins-base-1.26.5
9 MB /nix/store/ilk5qzvkadnj7lx58hfinfvl7jmhriq6-util-linux-2.41.3-bin
9 MB /nix/store/dml7jig0fh35chx530ga4icf68j4qayf-dejavu-fonts-2.37
9 MB /nix/store/c1yv698q09l7lay400v0zz9ajicmf65b-libreelec-dvb-firmware-1.5.0-zstd
9 MB /nix/store/abp49ljkaq0hnzfk0f016rzsrx4rhkx3-gst-plugins-base-1.26.5
9 MB /nix/store/7v308am31q0n3zk2361gqi5pv3swzrnw-groff-1.23.0
9 MB /nix/store/7nvb5clr8li75nn09gjqihy5mbx3g86i-libvpx-1.15.2
9 MB /nix/store/2y2faybcp9a3qgnivfmg4iznqvjcq6pa-zenity-4.2.1
8 MB /nix/store/ycv0bdf8bg06p8w6hhsk409c2q91clqb-svt-av1-3.1.2
The path shown is the current system generation. If you have multiple
generations, replace system with system-NN-link to compare.
You can also visualize the dependency graph (see annex):
focusing into ffmpeg
That’s strange, why does ffmpeg take “only” 28 MB but 247 MB in cumulative size ?
Let’s focus on it, showing even the small nodes in the path.
user profile
The user profile (from nix profile install) is often the biggest root
because it accumulates packages over time. Most of these packages come from
the hardliases system (~/prog/devel/bin/hardliases/) which lazily installs
them on first use. The home-manager-path entry is the closure of the
entire Home Manager environment (all packages from the HM config), not an
individual package. This breaks it down:
import json
import subprocess
if isinstance(profile_json, str):
data = json.loads(profile_json)
else:
data = json.loads("\n".join(str(x) for x in profile_json))
sizes = []
for name, elem in data.get("elements", {}).items():
for p in elem.get("storePaths", []):
r = subprocess.run(
["nix", "path-info", "--closure-size", p],
capture_output=True, text=True,
)
if r.returncode == 0:
parts = r.stdout.strip().split()
if len(parts) == 2:
sizes.append((name, int(parts[1])))
sizes.sort(key=lambda x: -x[1])
for name, s in sizes:
gb = s / 1024 / 1024 / 1024
if gb >= 0.1:
print(f" {name} -> {gb:.1f} GB")
else:
print(f" {name} -> {s // 1024 // 1024} MB")
flakes/android -> 8.3 GB
home-manager-path -> 7.3 GB
whisper-ctranslate2 -> 2.8 GB
blender -> 2.5 GB
mermaid-cli -> 2.0 GB
openboard -> 1.8 GB
cargo -> 1.6 GB
firefox -> 1.5 GB
handbrake -> 1.4 GB
vlc -> 1.3 GB
flakes/scrcpy -> 1.1 GB
openjdk -> 0.8 GB
libextractor -> 0.8 GB
bitwarden-cli -> 0.7 GB
deskflow -> 0.7 GB
apksigner -> 0.6 GB
apktool -> 0.6 GB
pdfarranger -> 0.6 GB
zbar -> 0.6 GB
zathura -> 0.5 GB
keepassxc -> 0.5 GB
fstl -> 0.4 GB
seahorse -> 0.4 GB
vault -> 0.4 GB
awscli2 -> 0.3 GB
feh -> 0.3 GB
pre-commit -> 0.3 GB
loudgain -> 0.3 GB
chromaprint -> 0.3 GB
argocd -> 0.3 GB
pdftotext -> 0.2 GB
typescript-language-server -> 0.2 GB
typescript -> 0.2 GB
flakes/colout -> 0.2 GB
pgcli -> 0.2 GB
pnpm -> 0.2 GB
eslint -> 0.2 GB
borgbackup -> 0.2 GB
msmtp -> 0.2 GB
flakes/renault-api -> 0.2 GB
httpie -> 0.2 GB
imagemagick -> 0.2 GB
consul -> 0.2 GB
flakes/rotate-backups -> 0.2 GB
deno -> 0.2 GB
mpremote -> 0.2 GB
w3m -> 0.2 GB
xvfb-run -> 0.1 GB
postgresql_16 -> 0.1 GB
graphviz -> 0.1 GB
poppler-utils -> 0.1 GB
kubo -> 0.1 GB
hugo -> 0.1 GB
ntfy-sh -> 0.1 GB
cups -> 0.1 GB
sox -> 0.1 GB
exiftool -> 0.1 GB
ledger -> 0.1 GB
sshfs -> 0.1 GB
uv -> 0.1 GB
nssTools -> 100 MB
util-linux -> 90 MB
gh -> 86 MB
ruff -> 84 MB
x11vnc -> 68 MB
nixfmt -> 68 MB
websocat -> 67 MB
dig -> 64 MB
ty -> 63 MB
devbox -> 62 MB
dagger -> 60 MB
kubectl -> 60 MB
libsecret -> 54 MB
terraform-ls -> 50 MB
fd -> 50 MB
stylua -> 50 MB
wget -> 47 MB
socat -> 46 MB
ncdu -> 46 MB
jless -> 46 MB
libfaketime -> 45 MB
flakes/oama -> 45 MB
crane -> 45 MB
lighttpd -> 43 MB
flakes/hld -> 42 MB
bkt -> 42 MB
openssl -> 41 MB
inotify-tools -> 41 MB
xclip -> 39 MB
flakes/phasher -> 39 MB
earthly -> 35 MB
xsel -> 35 MB
mpc -> 33 MB
sqlite -> 33 MB
dos2unix -> 33 MB
keyutils -> 33 MB
qrencode -> 31 MB
tree -> 31 MB
colorized-logs -> 31 MB
pstree -> 31 MB
openssl -> 3 MB
postgresql_16 -> 0 MB
util-linux -> 0 MB
kubectl -> 0 MB
cups -> 0 MB
sox -> 0 MB
keyutils -> 0 MB
borgbackup -> 0 MB
feh -> 0 MB
zbar -> 0 MB
sqlite -> 0 MB
qrencode -> 0 MB
You can also visualize the dependency graph (see annex):
Home Manager
Same idea for Home Manager. This closure often includes heavy packages like
chromium, emacs, qtwebengine, gcc (for native-comp), etc. The all entry
at the top is the aggregated derivation for the entire Home Manager profile,
not an individual package. Note: --size reports each path’s own size
(just the symlink farm), while the per-package breakdown above uses
--closure-size which includes all transitive dependencies — hence
home-manager-path appears much larger there.
nix path-info --size -r ~/.local/state/nix/profiles/home-manager | \
awk '{sz=$2/1024/1024; if (sz>=1024) printf "%.1f GB %s\n", sz/1024, $1; else printf "%.0f MB %s\n", sz, $1}' | sort -t' ' -k1 -rn | head -20
1015 MB /nix/store/1jncnixlflajcb85daq66kggsy2hb78f-all
649 MB /nix/store/dp52zh2z75dbd4nmy1wxc17msyygy2gm-chromium-unwrapped-145.0.7632.116
434 MB /nix/store/fa965xaq074a2c0v4d2ssrg02y5syl9v-qtwebengine-6.10.2
326 MB /nix/store/nqzrva0994k1xgh86kavhi5qcg54ixiq-emacs-30.2
264 MB /nix/store/sca0pf46jmxva40qahkcwys5c1lvk6n2-gcc-15.2.0
222 MB /nix/store/0qlmqvyckf8zyx03n00azgznnkzgi5xw-glibc-locales-2.42-51
181 MB /nix/store/7hgr71y4mchf0h4w4i36j49lhvzl4nc2-docker-scout-1.20.2
165 MB /nix/store/4z51xyah9h8h3al1wclvgy6cb04vq0vl-qtdeclarative-6.10.2
141 MB /nix/store/qx6qwzhfxqskg87zl9igij2nbk0kclmk-deno-2.6.10
139 MB /nix/store/6n4hzs2swrk7hy8v36hs118n33sw7qz4-libgccjit-15.2.0
107 MB /nix/store/m1fw8l8y9ycxh5dzispbb7cwl6rra14l-python3-3.13.12
99 MB /nix/store/iarcrhvmxrk4hfqqhy9di2kagb2rjwji-moby-29.2.1
98 MB /nix/store/9g44xzv8bxq3n9pg3h7wjjcbqg70gy4l-docker-containerd-29.2.1
86 MB /nix/store/rkh0rc2nhdy7iian5vdhbsb8fmy0x21x-docker-buildx-0.31.1
85 MB /nix/store/q62chy9hy3wfyb63q166dg1xldl506m1-index-x86_64-linux
84 MB /nix/store/yricn9c0wsv39r324dm96bsyfqvsdp3p-samba-4.22.7
83 MB /nix/store/w4q31b93w262q2b75ri3jc7m3xd4i31h-qtbase-6.10.2
80 MB /nix/store/h4apbnz8p5fmjwcmzlaxcjm5vr70d97x-nodejs-slim-24.13.0
76 MB /nix/store/gsykwaghk72cm2wwfcxpznr0i3yp9020-opencv-4.13.0
61 MB /nix/store/crabwpx7qyz7rysfdvsfg4dclk811q9d-xscreensaver-6.13
You can also visualize the dependency graph (see annex):
Find dangling result symlinks
nix build creates result symlinks that register as GC roots. They
accumulate in project directories and keep old builds alive. Note: some
hardliases use custom flakes (e.g. scrcpy builds from
~/prog/devel/flakes/scrcpy) — if you run nix build manually in those
flake directories, the result symlink will linger there too.
nix-store --gc --print-roots 2>&1 | grep -v '/proc/' | grep 'result'
To remove them (review the list first!):
roots=$(nix-store --gc --print-roots 2>&1 | grep -v '/proc/' | grep 'result' | awk '{print $1}')
for r in $roots; do
if [ "$DRY_RUN" = "yes" ]; then
echo "[dry run] would remove: $r"
else
rm -v "$r"
fi
done
Set DRY_RUN to "no" in the header to actually delete them.
Find /tmp GC roots
Same problem as result symlinks, but in /tmp. The symlinks themselves may
be gone after reboot, but the GC root entries in
/nix/var/nix/gcroots/auto/ persist. nix-collect-garbage cleans stale
roots automatically, but it helps to know they’re there:
nix-store --gc --print-roots 2>&1 | grep -v '/proc/' | grep '/tmp/'
Check for reclaimable space (dry run)
Before actually collecting, see how much would be freed:
nix-collect-garbage --dry-run 2>&1 | tail -3
If this shows nothing, the store is already clean — all paths are live.
Cleaning up
Remove old NixOS generations
List current generations:
nix-env --list-generations --profile /nix/var/nix/profiles/system
83 2026-03-25 11:15:33
84 2026-03-29 11:51:21 (current)
Delete all but the current one:
nix-collect-garbage -d
Or keep the last 7 days:
nix-collect-garbage --delete-older-than 7d
Remove old Home Manager generations
List current generations:
home-manager generations
Expire old ones (keep last 7 days):
home-manager expire-generations "-7 days"
Remove old user profile generations
Every time a hardlias triggers nix profile add, a new generation is
created. Over time this accumulates many generations, each holding a full
closure. Wiping history is safe — it only removes old generations, not the
current one. All installed packages remain available.
nix profile wipe-history --profile ~/.local/state/nix/profiles/profile
Or keep recent ones:
nix profile wipe-history --profile ~/.local/state/nix/profiles/profile --older-than 7d
You can also remove individual packages to save space. The hardlias wrapper will reinstall it transparently the next time you run the command (at the cost of a one-time rebuild):
nix profile remove --profile ~/.local/state/nix/profiles/profile "$PKG"
Garbage collect the store
This removes all store paths not reachable from any GC root (including stale roots pointing to deleted symlinks):
nix-collect-garbage
For a more aggressive clean (also removes old generations from all profiles
system-wide — use --delete-older-than variant above if you want more
control):
nix-collect-garbage -d
Rebuild boot entries
After removing old system generations, the boot loader (systemd-boot) may
still have stale entries. The generation files in /nix/var/nix/profiles/ are
gone, but the entries in /boot/loader/entries/ are not cleaned automatically
by garbage collection. Regenerate them:
boot_entries=$(ls /boot/loader/entries/ | wc -l)
generations=$(nix-env --list-generations --profile /nix/var/nix/profiles/system | wc -l)
echo "boot entries: $boot_entries"
echo "system generations: $generations"
if [ "$boot_entries" -gt "$generations" ]; then
echo "=> STALE: $((boot_entries - generations)) extra boot entries to clean"
else
echo "=> OK"
fi
boot entries: 2
system generations: 2
=> OK
If stale, rebuild:
/run/current-system/bin/switch-to-configuration boot
This regenerates boot entries to match only the existing generations. Verify:
ls /boot/loader/entries/ | wc -l
The change takes effect on next reboot.
Which boot loader am I using?
if [ -d /boot/loader/entries ]; then
echo "systemd-boot"
elif [ -d /boot/grub ]; then
echo "GRUB"
else
echo "unknown — check /boot/"
fi
systemd-boot
GRUB variant
If using GRUB instead of systemd-boot, the rebuild command is different:
nixos-rebuild boot
Verify
After cleanup, check the final size and confirm nothing is left to collect:
du -sh /nix
echo "---"
nix-collect-garbage --dry-run 2>&1 | tail -3
Annex
Dependency graph code
A visual graph of the dependency tree makes it easy to spot which paths pull in the most weight. Each node shows the package name and its own size; edges show “depends on”. Only paths above a configurable threshold are included to keep the graph readable.
Change root to the root you want to explore (a GC root, a profile,
or any store path). Change minmb to filter out small nodes. Set focus
to a package name to restrict the graph to that package’s closure.
The minmb threshold applies differently depending on the mode:
- Without focus: filters by own size (
narSize). This keeps the graph small and readable on large closures like the full system. - With focus: filters by closure size. When zooming into a single package, you want to see the full dependency chain — including small intermediary packages that pull in heavy transitive dependencies.
The graph is color-coded:
- Red: ≥500 MB
- Orange: ≥200 MB
- Yellow: ≥100 MB
- Green: <100 MB (but above the threshold)
The main entry point assembles all the pieces:
import json
import subprocess
import re
import os
min_bytes = int(minmb) * 1024 * 1024
store_path = os.path.expanduser(root)
r = subprocess.run(
["nix", "path-info", "-r", "--json", "--closure-size", store_path],
capture_output=True, text=True,
)
if r.returncode != 0:
raise RuntimeError(f"nix path-info failed: {r.stderr}")
raw = json.loads(r.stdout)
# nix path-info --json returns a dict keyed by store path
if isinstance(raw, list):
info = {p["path"]: p for p in raw}
else:
info = {path: data for path, data in raw.items()}
def closure_size(path):
return info[path].get("closureSize", 0)
if focus:
focus_path = [p for p in info if re.search(re.escape(focus), p)]
if not focus_path:
raise RuntimeError(f"no store path matching '{focus}'")
focus_path = focus_path[0]
relevant = set()
stack = [focus_path]
while stack:
p = stack.pop()
if p in relevant or p not in info:
continue
relevant.add(p)
for ref in info[p].get("references", []):
if ref not in relevant:
stack.append(ref)
info = {p: info[p] for p in relevant if p in info}
if focus:
big = {path for path in info if closure_size(path) >= min_bytes}
else:
big = {path for path in info if info[path].get("narSize", 0) >= min_bytes}
def fmt_size(size):
if size >= 1024 * 1024 * 1024:
return f"{size / 1024 / 1024 / 1024:.1f} GB"
elif size >= 1024 * 1024:
return f"{size // (1024 * 1024)} MB"
else:
return f"{size // 1024} KB"
def label(path):
hash_prefix = re.search(r"^/nix/store/([a-z0-9]{8})", path).group(1)
name = re.sub(r"^/nix/store/[a-z0-9]+-", "", path)
own = info[path].get("narSize", 0)
clos = closure_size(path)
return f"{name} ({hash_prefix})\\n{fmt_size(own)} (closure: {fmt_size(clos)})"
def color(path):
size = info[path].get("narSize", 0)
mb = size / 1024 / 1024
if mb >= 500:
return "#e74c3c" # red
elif mb >= 200:
return "#e67e22" # orange
elif mb >= 100:
return "#f1c40f" # yellow
else:
return "#2ecc71" # green
lines = [
'digraph deps {',
' rankdir=LR;',
' node [shape=box, style="filled,rounded", fontname="sans-serif", fontsize=10];',
' edge [color="#bbbbbb"];',
]
for path in sorted(big):
node_id = path.replace("/", "_").replace("-", "_").replace(".", "_")
lines.append(
f' "{node_id}" [label="{label(path)}", fillcolor="{color(path)}"];'
)
for path in sorted(big):
src = path.replace("/", "_").replace("-", "_").replace(".", "_")
for ref in sorted(info[path].get("references", [])):
if ref != path and ref in big:
dst = ref.replace("/", "_").replace("-", "_").replace(".", "_")
edge_size = fmt_size(closure_size(ref))
lines.append(f' "{src}" -> "{dst}" [label="+{edge_size}", fontsize=8];')
lines.append("}")
dot = "\n".join(lines)
subprocess.run(
["dot", "-Tsvg", "-o", out],
input=dot,
text=True, check=True,
)
return out
First we ask nix for the full closure of the root path. The
--closure-size flag makes nix compute each path’s transitive closure
size natively, which is much faster than recomputing it in Python.
r = subprocess.run(
["nix", "path-info", "-r", "--json", "--closure-size", store_path],
capture_output=True, text=True,
)
if r.returncode != 0:
raise RuntimeError(f"nix path-info failed: {r.stderr}")
raw = json.loads(r.stdout)
# nix path-info --json returns a dict keyed by store path
if isinstance(raw, list):
info = {p["path"]: p for p in raw}
else:
info = {path: data for path, data in raw.items()}
def closure_size(path):
return info[path].get("closureSize", 0)
When focus is set, we restrict the graph to only the closure of the
matching package. This lets you zoom into a single package to understand
why it pulls in so much. We walk the reference graph with BFS to find
all transitive dependencies of the focused path.
if focus:
focus_path = [p for p in info if re.search(re.escape(focus), p)]
if not focus_path:
raise RuntimeError(f"no store path matching '{focus}'")
focus_path = focus_path[0]
relevant = set()
stack = [focus_path]
while stack:
p = stack.pop()
if p in relevant or p not in info:
continue
relevant.add(p)
for ref in info[p].get("references", []):
if ref not in relevant:
stack.append(ref)
info = {p: info[p] for p in relevant if p in info}
In focus mode, we filter by closure size so that small intermediary packages that pull in heavy dependencies remain visible — this is the whole point of zooming in. On the full system graph, we filter by own size to keep the graph readable and fast to render.
if focus:
big = {path for path in info if closure_size(path) >= min_bytes}
else:
big = {path for path in info if info[path].get("narSize", 0) >= min_bytes}
Formatting helpers to make the graph readable: human-friendly sizes and color coding by own size to quickly spot the heaviest individual paths.
def fmt_size(size):
if size >= 1024 * 1024 * 1024:
return f"{size / 1024 / 1024 / 1024:.1f} GB"
elif size >= 1024 * 1024:
return f"{size // (1024 * 1024)} MB"
else:
return f"{size // 1024} KB"
def label(path):
hash_prefix = re.search(r"^/nix/store/([a-z0-9]{8})", path).group(1)
name = re.sub(r"^/nix/store/[a-z0-9]+-", "", path)
own = info[path].get("narSize", 0)
clos = closure_size(path)
return f"{name} ({hash_prefix})\\n{fmt_size(own)} (closure: {fmt_size(clos)})"
def color(path):
size = info[path].get("narSize", 0)
mb = size / 1024 / 1024
if mb >= 500:
return "#e74c3c" # red
elif mb >= 200:
return "#e67e22" # orange
elif mb >= 100:
return "#f1c40f" # yellow
else:
return "#2ecc71" # green
We emit Graphviz DOT syntax: one node per big path, one edge per direct reference between big paths. Edge labels show the closure size of the target to help estimate how much removing that dependency would save.
lines = [
'digraph deps {',
' rankdir=LR;',
' node [shape=box, style="filled,rounded", fontname="sans-serif", fontsize=10];',
' edge [color="#bbbbbb"];',
]
for path in sorted(big):
node_id = path.replace("/", "_").replace("-", "_").replace(".", "_")
lines.append(
f' "{node_id}" [label="{label(path)}", fillcolor="{color(path)}"];'
)
for path in sorted(big):
src = path.replace("/", "_").replace("-", "_").replace(".", "_")
for ref in sorted(info[path].get("references", [])):
if ref != path and ref in big:
dst = ref.replace("/", "_").replace("-", "_").replace(".", "_")
edge_size = fmt_size(closure_size(ref))
lines.append(f' "{src}" -> "{dst}" [label="+{edge_size}", fontsize=8];')
lines.append("}")
dot = "\n".join(lines)
Finally, render the DOT graph to SVG using Graphviz.
subprocess.run(
["dot", "-Tsvg", "-o", out],
input=dot,
text=True, check=True,
)
return out