Achieving deterministic NuGet restores with lock files
Learn how NuGet's lock file (packages.lock.json) creates deterministic package restores, see a worked example, and understand the limits and verification steps.
05 Aug 2026, 06:44 UTC

Problem: non‑deterministic restores
When a team builds the same solution on different machines or at different times, NuGet may resolve slightly different versions of transitive dependencies. This can lead to "works on my machine" bugs, slower builds due to repeated version resolution, and uncertainty about which binaries are actually being used.
How the lock file solves it
NuGet’s lock file (packages.lock.json) records the exact version, content hash, and package source for every package and its transitive dependencies that were resolved during a restore. When the file exists and locked‑mode is enabled, NuGet skips version resolution and directly downloads the recorded packages, guaranteeing identical binaries across restores.
Enabling lock‑file mode
For SDK‑style projects (the default for .NET SDK 6.0+), you can opt‑in by setting the MSBuild property RestoreLockedMode to true or by using the CLI flag --locked-mode. The lock file is generated automatically on the first restore in this mode and should be committed to source control.
Worked example
- Create a new class library:
dotnet new classlib -n LockDemo - Move into the folder:
cd LockDemo - Add a package that uses a version range (e.g.,
Newtonsoft.Json):dotnet add package Newtonsoft.Json --version 13.0.0 - Run a locked restore:
dotnet restore --locked-mode - Observe that a
packages.lock.jsonappears next to the project file. Its content (illustrative only) might look like:
{
"version": 1,
"dependencies": {
"Newtonsoft.Json": {
"requested": "13.0.0",
"resolved": "13.0.3",
"hash": "sha512-...",
"sources": ["https://api.nuget.org/v3/index.json"]
}
}
}- Delete the
objfolder to simulate a clean environment:rm -rf obj - Restore again without the flag (the lock file will be used automatically):
dotnet restore - Check the output – you should see no version resolution logs and the restored packages match the exact versions recorded in the lock file.
Updating dependencies
When you intentionally change a direct or transitive dependency, you must regenerate the lock file. Run dotnet restore --locked-mode again; NuGet will resolve the new versions and overwrite packages.lock.json. Committing the updated lock file ensures the whole team gets the same new set of binaries.
Trade‑offs and limitations
- Maintenance overhead: Forgetting to regenerate the lock file after a dependency change can lock in outdated or vulnerable packages.
- Platform specifics: The lock file records the package hash but does not capture runtime RID or native asset variations; platform‑specific issues can still appear.
- Compatibility: Classic
packages.configprojects require theNuGet.exeCLI to generate lock files; SDK‑style projects work with thedotnetCLI.
Practical verification
To confirm that a locked restore is truly deterministic:
- Commit the current
packages.lock.jsonto a fresh clone of the repository. - Delete all
objandbinfolders. - Run
dotnet restore(no flag). - Compare the restored package folders under
~/.nuget/packages(or the local nuget cache) with the hashes listed in the lock file – they should match exactly.
If the hashes match, you have verified that the lock file is delivering deterministic binaries.
Closing
Adopting NuGet lock files gives teams a reliable way to eliminate surprise version changes, speed up restores, and maintain build consistency across environments. The trade‑off is a small amount of extra vigilance: treat the lock file as any other source‑controlled artifact and regenerate it whenever you change dependencies. With that habit in place, your builds become predictable and your CI pipelines more stable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.