August 21, 2025·4 min read·engineeringnixbuildsssh

Nix 2.31 resolves CPU counts before invoking builders

Nix 2.31 turns a zero core setting into an available CPU count before invoking a builder. SSH transport fixes and cache concurrency changes address other boundaries where callers relied on implicit behavior.

A zero used as an automatic setting still needs to become a real resource count somewhere. Before Nix 2.31, explicitly selecting build-cores = 0 passed that zero to the builder, leaving the builder to interpret it. The new release detects the available CPU count and supplies that number through NIX_BUILD_CORES instead.

The August 21 release notes say this now matches the behavior when the setting is unset. They also include fixes for Git LFS over SSH and store URLs with explicit ports. The common issue is how Nix translates a declared setting into the concrete arguments or environment another program receives.

I use Nix to make machine configuration explicit, but an automatic value is still a policy. Detecting available CPUs provides a concrete answer to a builder. It does not establish how much of a shared machine that build should consume, which remains a separate configuration decision.

A builder receives a count rather than a sentinel

The versioned core-setting definition describes the value passed through NIX_BUILD_CORES. This is the number of cores a builder should use, if supported. The separate max-jobs setting controls how many local Nix builds run concurrently.

Those settings describe different kinds of parallel work. One build can run multiple compilation tasks internally. Nix can also schedule several builds at once. A configuration that considers only one setting does not completely describe the resulting resource demand.

The new zero handling moves detection to Nix. Builders no longer need to agree on how to interpret zero in this environment variable. An explicit positive value still expresses a limit chosen by the operator, while zero requests the available CPU count detected for the build environment.

This is useful for consistency, but it is not a machine-wide resource reservation. The documented setting communicates a count to builders that support it. It does not claim that every build system obeys it or that multiple concurrent builds coordinate their internal work through that value alone.

The distinction matters on a machine doing other work. Automatically detecting every available CPU can be appropriate for a dedicated builder and excessive for a shared workstation. The release fixes the interpretation of the automatic setting without deciding which policy fits every host.

The setting documentation gives a concrete Nixpkgs example. When mkDerivation has enableParallelBuilding = true, the build helper passes -j with NIX_BUILD_CORES to GNU Make. That extra condition explains why setting a count alone does not guarantee parallel execution in every package. Nix supplies the detected environment value to the builder, while the package's builder determines whether and how it becomes a command-line limit. The new behavior standardizes the value at the first boundary without replacing the builder's own configuration.

Git LFS needs both authentication and the right endpoint

Git LFS fetching over SSH receives several related corrections. The release adds support for NIX_SSHOPTS, uses the port parsed from the URL, and makes better use of the response from git-lfs-authenticate when the API is not on port 443.

These are separate requirements in a successful fetch. SSH options can describe how to reach or authenticate to the service. The URL supplies a port. The authentication response supplies information about the LFS API endpoint. Losing one part can make an otherwise valid repository reference fail.

The endpoint correction is particularly informative. A successful SSH authentication step does not prove the subsequent API request targets the correct address and port. A fetcher needs to preserve the server-provided relationship between authentication and the API it authorizes.

This is an interoperability fix at the fetch boundary. It does not alter which source a flake intends to select. It repairs how the implementation reaches that source when the server uses transport settings beyond the most common defaults.

Store references can carry the SSH port directly

The release also fixes explicit ports in SSH store URIs. Earlier Nix versions passed the port together with the host name to the SSH executable. Users could work around that through SSH configuration or NIX_SSHOPTS, but the URI itself did not work as intended.

A store reference can now express a loopback service on a separate port directly.

ssh://127.0.0.1:2222

The notes say the correction applies to store references in command flags and remote-builder configuration. That makes the connection endpoint part of the reference instead of requiring an unrelated host alias or environment setting to supply the port.

Scoped IPv6 addresses receive a separate syntax correction. A zone identifier's percent sign must now be percent-encoded as %25 inside a URI. The notes explain that a literal percent sign was ambiguous because URI syntax already reserves it for encoding. This is a reference-syntax change, distinct from the SSH argument construction fix.

Cache concurrency affects simultaneous commands

Nix 2.31 changes SQLite caches from truncate mode to WAL mode. The release notes identify the flake evaluation cache as a particular problem because its transactions can run for a long time. Another Nix command accessing the same cache could block for an unbounded period while a transaction was in progress.

The change updates cache versions, including moving the evaluation cache filename from version 5 to version 6. That is an implementation-cache transition, separate from the lock file that declares a flake's input graph. A new cache version does not mean the project's dependency selection has changed.

The release also introduces nix flake prefetch-inputs to fetch a flake's inputs in parallel. Its documented tradeoff is that it can fetch inputs regular evaluation would never use. Parallel preparation can reduce waiting, while doing more total fetch work than demand-driven evaluation.

For me, the useful part of these changes is knowing which stage each one controls. Builder core counts affect work inside derivations, prefetching affects source availability, and WAL affects contention between local commands. The settings and fixes become easier to use when those boundaries remain explicit in the explanation of a slow or failed invocation.