nix Didn't Hang. It Was Evaluating the World! — LabCraft BlogStart your first lab →
BlogManifestoContactStart your first lab →
nix search nixpkgs jq looks like a small command.
It is often the first Nix command after installation that feels useful: ask<br>nixpkgs for a package, find the attribute path, then use that path in<br>nix shell, nix run, or a flake. As a teaching moment, it is excellent. It<br>connects the package set in the abstract to something a learner can run.
If you are newer to Nix, there are three moving parts hiding in that line.<br>nixpkgs is the large package collection most Nix users start from. It is not<br>just a database of package names ; it is a large body of Nix code that<br>describes packages, versions, metadata, dependencies, and build plans.<br>nix search searches package names and descriptions, but to do that for a flake<br>input it may need to evaluate package metadata. Nix keeps a local sqlite-backed<br>eval cache so the next search can reuse work the first search already paid for.
Then we tried it in a 2 GiB Linux VM and Nix got OOM-killed.
The failure was not mysterious once we measured it. With Nix's eval cache<br>disabled, nix search nixpkgs jq averaged 3865.4 MiB peak RSS across three<br>runs on a high-memory x86_64 Linux builder. In the 2 GiB training VM, the same<br>kind of cold evaluation ran out of room: the kernel killed nix around 1.85 GiB<br>resident.
The learner watched Nix churn, then stop:
sh<br>$ nix search nixpkgs jq<br>unpacking 'github:NixOS/nixpkgs/50ab7937…' into the Git cache...<br>evaluation warning: CUDA versions older than 12.0 will be removed in Nixpkgs 25.05; …<br>evaluation warning: marwaita-manjaro has been renamed to marwaita-teal<br>evaluation warning: kernelPackages.system76-power is now pkgs.system76-power<br>Killed
That is a bad first hour with Nix.
Not because every real-world Nix user runs inside a 2 GiB VM. Most do not. The<br>point of a training VM is different: it should constrain the world enough that a<br>learner can break real systems without risking their own machine. However, when<br>the environment fails before the lesson does, that is a bug on our end.
nix did not hang
This failure sits next to a broader Nix UX problem: Nix often goes quiet while<br>it is doing real work. To an experienced user, that silence may mean "the<br>evaluator is walking a large expression graph." To a beginner, it looks like a<br>hang.
nix search is a perfect example. The command sounds like a lookup. In<br>practice, the first run can evaluate enough of nixpkgs to become CPU-heavy,<br>memory-heavy, and quiet for long enough that the user loses confidence in the<br>command. If it eventually prints results, the lesson survives. If the kernel<br>kills it, the lesson becomes "nix froze and died."
We cannot fix Nix's progress reporting from a training VM. But we can make the<br>environment pay the evaluation cost before the learner asks for it.
The pre-warmed path averaged 586.5 MiB peak RSS across the same three-run<br>measurement. That is not a best-case "cache-only" microbenchmark: every run got<br>a fresh home directory, fresh XDG cache/config/state directories, and only the<br>baked eval-cache sqlite was seeded. Nix still had to unpack the pinned nixpkgs<br>source into its Git cache. That makes the measurement closer to first-use<br>behavior than a warmed developer shell.
Even with that stricter setup, the warm path used about 6.6x less memory<br>than the cold path and stayed well below the 2 GiB VM cap.
The 512 MiB line in the chart is an aggressive stretch budget we used while<br>designing the guard. The product constraint is less romantic: the command has to<br>finish reliably in the small VM. The cold path cannot. The pre-warmed path can.
The visible command could not change
For LabCraft, the install step matters because the training environment is a<br>real Linux VM, not an emulator. A learner should still install Nix, see what<br>lands on the system, inspect the daemon, and later uninstall it from the<br>installer receipt. Hiding Nix in the base image would remove the failure, but it<br>would also remove the lesson.
So the visible command stayed unchanged:
sh<br>curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install --no-confirm
That ruled out the obvious pinning path: changing the URL to a tagged<br>Determinate installer URL. It would work technically, but it would teach a<br>special command.
Instead, we pinned the environment around the command.
The Determinate installer script honors environment variables. In a real VM, a<br>preloaded image layer can seed /etc/profile.d before the shell opens. The<br>learner still runs the same command, but the shell already carries the<br>environment that makes the install deterministic.
The first tempting variable was NIX_INSTALLER_NIX_PACKAGE_URL. That can point<br>the installer at an upstream Nix tarball, and it does pin a version. But it also<br>drops the Determinate Nix distribution. In our test, every nix command then<br>printed warnings for Determinate-specific settings such as...