The Cargo Guide

Lockfiles and Reproducibility

Why Cargo.lock Exists

Cargo resolves dependency requirements like serde = "1" into exact package versions. Cargo.lock records those exact choices so future builds keep using the same resolved graph instead of re-resolving to whatever compatible versions happen to be newest at that moment.

A useful mental model is:

  • Cargo.toml expresses version policy
  • Cargo.lock records the concrete dependency graph actually chosen

For example, this manifest is intentionally flexible:

[dependencies]
serde = "1"
regex = "1"

But once Cargo resolves it, Cargo.lock pins the exact versions for repeatable builds.

Manifest Policy vs Lockfile Reality

Beginners often think Cargo.toml alone determines a completely reproducible build. It does not. Cargo.toml defines what versions are allowed, while Cargo.lock records which exact versions were selected.

Conceptually:

Cargo.toml  -> acceptable version ranges
Cargo.lock  -> exact resolved versions

That distinction matters because a flexible manifest without a lockfile can lead to different concrete builds over time.

What Reproducibility Means in Practice

In Cargo workflows, reproducibility usually means that the same package resolves to the same dependency versions and therefore builds against the same graph across machines or over time.

A lockfile is one of the main tools that makes this possible.

Typical local workflow:

cargo build
cargo test
git add Cargo.lock

Typical CI expectation:

cargo build --locked
cargo test --locked

The broad idea is simple: the lockfile reduces surprise.

A Small Example Package

Suppose you create a package like this:

cargo new lock_demo
cd lock_demo

And add dependencies:

[package]
name = "lock_demo"
version = "0.1.0"
edition = "2024"
 
[dependencies]
serde = "1"
regex = "1"

A simple library function might look like this:

use regex::Regex;
 
pub fn has_number(s: &str) -> bool {
    Regex::new(r"\d").unwrap().is_match(s)
}

The first build resolves and records exact versions:

cargo build

After that, Cargo.lock becomes part of the package's reproducibility story.

Application vs Library Norms

A common Cargo convention is that applications usually commit Cargo.lock, while libraries have historically been more mixed in practice because library consumers do not use the library author's lockfile when the library is used as a dependency.

A practical mental model is:

  • application: the lockfile usually represents the actual shipped build graph, so committing it is usually expected
  • library: the lockfile can still be useful for local development and CI, even though downstream users resolve their own graphs when they depend on the library

This is why you will often see application repositories treat Cargo.lock as essential source-controlled infrastructure.

Why Libraries Still Care About Cargo.lock

Even though a published library's downstream users resolve their own dependency graphs, the library author still benefits from a lockfile locally.

It helps with:

  • stable CI behavior
  • team consistency during development
  • debugging regressions against a known graph
  • controlled updates through explicit lockfile changes

So the lockfile is not only for final applications. It is also useful during library maintenance.

Workspace Lockfiles

In a Cargo workspace, all packages share a common Cargo.lock file at the workspace root.

Example layout:

my_workspace/
ā”œā”€ā”€ Cargo.toml
ā”œā”€ā”€ Cargo.lock
ā”œā”€ā”€ app/
│   ā”œā”€ā”€ Cargo.toml
│   └── src/main.rs
└── core/
    ā”œā”€ā”€ Cargo.toml
    └── src/lib.rs

Workspace manifest:

[workspace]
members = ["app", "core"]
resolver = "3"

This shared lockfile matters because the workspace is developed as one coordinated graph rather than as isolated packages.

What --locked Does

The --locked flag tells Cargo to use the existing lockfile exactly as-is and fail if Cargo would need to change it.

Examples:

cargo build --locked
cargo test --locked
cargo check --locked

A useful practical interpretation is:

  • if Cargo.lock is missing, Cargo errors
  • if dependency resolution would change Cargo.lock, Cargo errors

This makes --locked a strong fit for CI and any workflow that wants deterministic dependency selection.

What --offline Does

The --offline flag tells Cargo not to access the network.

Examples:

cargo build --offline
cargo test --offline

This is useful when:

  • you are on a machine without network access
  • you want to ensure the build uses only locally available dependencies
  • you are testing offline or hermetic workflow assumptions

A critical subtlety is that offline mode is not just "slower online mode without downloads." It can lead to different behavior because Cargo must restrict itself to already-available local data.

What --frozen Does

The --frozen flag combines the effect of --locked and --offline.

Examples:

cargo build --frozen
cargo test --frozen

A helpful mental model is:

  • --locked: do not change the lockfile
  • --offline: do not touch the network
  • --frozen: enforce both at once

This is especially common in tightly controlled CI or release workflows.

Using cargo fetch Before Going Offline

If you want reliable offline builds, it is often useful to prefetch dependencies first.

cargo fetch
cargo build --offline

If a lockfile already exists, cargo fetch downloads the dependencies needed for that lockfile. If no lockfile exists, it will generate one before fetching.

This makes cargo fetch a good preparatory step before disconnecting from the network or moving into a more restricted build environment.

A Practical Offline Workflow

A simple team-friendly pattern is:

cargo fetch
cargo build --locked
cargo test --locked

Then later, in an offline environment:

cargo build --frozen
cargo test --frozen

The key idea is to separate two concerns:

  • first ensure the graph and artifacts are locally available
  • then enforce reproducibility and no-network guarantees

Reproducible Local Builds

For local development, reproducibility usually means you can come back later, rebuild, and still be using the same dependency graph unless you explicitly decide to update it.

A stable local flow often looks like this:

cargo build
cargo test
git diff Cargo.lock

If Cargo.lock changed unexpectedly, that is usually a signal worth understanding before committing.

Reproducible CI Builds

CI systems usually want dependency resolution to be explicit and stable.

A common pattern is:

#!/usr/bin/env sh
set -e
 
cargo build --locked
cargo test --locked
cargo doc --locked

And in more restricted pipelines:

#!/usr/bin/env sh
set -e
 
cargo build --frozen
cargo test --frozen

These workflows reduce the chance that a pipeline silently picks up a newer compatible dependency version than the one developers tested locally.

Lockfile Churn

Lockfile churn means frequent or noisy changes to Cargo.lock that are larger or less intentional than they need to be.

This often happens when:

  • dependency updates are allowed to drift casually
  • multiple branches update overlapping parts of the dependency graph
  • new features or platforms cause resolution changes
  • teams update manifests without a clear lockfile policy

A lockfile diff is not automatically a problem, but it is often a meaningful artifact that deserves review rather than being treated as noise.

Reducing Lockfile Churn

A few habits reduce unnecessary lockfile churn.

Keep updates intentional:

cargo update

Or update one package deliberately:

cargo update -p regex

Then run the normal verification loop:

cargo test
cargo build

The key idea is that the lockfile should change because you meant to change the graph, not because resolution drifted in an uncontrolled way.

Minimal Lockfile Included in Packages

When a package with a binary executable target is published, Cargo can include a Cargo.lock file in the packaged crate. That packaged lockfile is useful for reproducible installation of that published package.

A practical consequence appears with cargo install.

By default:

cargo install some_binary_crate

Cargo may ignore the packaged lockfile and recompute dependency resolution.

To force use of the packaged lockfile when available:

cargo install --locked some_binary_crate

That is useful when you want installation to reproduce the dependency graph that was packaged with the published binary crate.

Why Packaged Lockfiles Matter More for Binary Crates

Binary crates are closer to finished applications than reusable library crates. That makes their packaged lockfiles especially relevant when users install them directly.

A helpful mental model is:

  • library dependency use: downstream packages resolve their own graph
  • installed binary package: using the packaged lockfile can better reproduce the author's intended tested graph

Team Workflows Around Cargo.lock

Healthy team workflows usually treat the lockfile as a shared artifact rather than a disposable byproduct.

Typical team practices include:

  • committing Cargo.lock for applications and workspaces
  • reviewing lockfile diffs when dependencies change
  • using --locked in CI
  • updating dependencies intentionally rather than incidentally

A simple workflow might look like this:

cargo update -p serde
cargo test
git add Cargo.toml Cargo.lock
git commit -m "Update serde"

This turns dependency movement into an explicit team event.

Merge Conflicts in Cargo.lock

Teams frequently encounter lockfile merge conflicts because many branches may change dependency resolution.

A common practical resolution pattern is:

git checkout --theirs Cargo.lock
cargo build
cargo test

Or sometimes:

rm Cargo.lock
cargo generate-lockfile
cargo test

The exact strategy depends on context, but the main lesson is that the correct conflict resolution is usually the one that restores a valid, tested lockfile for the merged manifest state.

Generating or Regenerating the Lockfile

Cargo can generate or rebuild a lockfile directly.

cargo generate-lockfile

And dependency updates can also create or refresh it:

cargo update

These commands are useful when:

  • the lockfile is missing
  • you intentionally want to refresh the resolved graph
  • you need to recover from a broken or stale lockfile state

How Long-Lived Environments Behave Differently

On machines that build Rust projects over long periods, Cargo's caches matter almost as much as the lockfile.

A lockfile may describe a stable dependency graph, but a long-lived environment still depends on cached registry, git, and package data being available locally when using restricted modes like --offline or --frozen.

That is why cache behavior is part of reproducibility in practice, especially for developer laptops, CI runners, and long-lived build hosts.

Automatic Cache Garbage Collection

Cargo now includes automatic garbage collection for its global caches. The purpose is to stop Cargo's home directory from growing without bound over time.

At a practical level, this means long-lived environments may eventually lose old cached dependency artifacts if they have not been used for long enough.

That is usually a good thing for disk usage, but it means reproducibility assumptions should not quietly depend on caches living forever.

Why Automatic Cache GC Affects Reproducibility Planning

Suppose a machine built a project months ago and later tries to rebuild it with strict offline assumptions.

If old cached artifacts have been garbage-collected, this may affect whether --offline or --frozen workflows still succeed without a prior refresh.

That means a strong reproducibility strategy should rely on more than hope that old caches still exist. Teams that need robust offline or hermetic behavior often use one or more of:

  • cargo fetch as a preparation step
  • vendoring
  • controlled CI cache warming
  • mirrors or internal registries

A Practical CI Pattern with Locking and Prefetching

A more deliberate CI sequence can look like this:

cargo fetch
cargo build --locked
cargo test --locked

And for stricter environments:

cargo fetch
cargo build --frozen
cargo test --frozen

The first line ensures required dependencies are locally available. The later lines enforce graph stability.

Common Beginner Mistakes

Mistake 1: treating Cargo.lock as redundant because dependencies are already listed in Cargo.toml.

Mistake 2: assuming --locked and --offline mean the same thing.

Mistake 3: using --offline without first ensuring dependencies are locally available.

Mistake 4: treating lockfile diffs as meaningless noise.

Mistake 5: assuming long-lived machines will keep cached dependency data forever.

Mistake 6: thinking library authors never benefit from a lockfile.

The lockfile remains useful for local development, CI, and debugging even when downstream library consumers do not use that exact file.

Hands-On Exercise

Create a small package and walk through the lockfile workflow deliberately.

Start here:

cargo new lockfile_lab
cd lockfile_lab

Add dependencies:

[dependencies]
serde = "1"
regex = "1"

Build once to create the lockfile:

cargo build

Now try a reproducibility-oriented sequence:

cargo test --locked
cargo fetch
cargo build --offline
cargo test --frozen

Then inspect what changed:

git diff Cargo.lock

This exercise helps make the differences between normal builds, locked builds, offline builds, and frozen builds concrete.

Mental Model Summary

A strong mental model for lockfiles and reproducibility in Cargo is:

  • Cargo.toml defines allowed dependency ranges
  • Cargo.lock records the exact resolved graph
  • applications and workspaces usually rely heavily on committed lockfiles
  • libraries still benefit from lockfiles for local development and CI
  • --locked forbids lockfile drift
  • --offline forbids network access
  • --frozen enforces both
  • cargo fetch prepares dependencies for restricted or offline workflows
  • packaged lockfiles are especially relevant for published binary crates and cargo install --locked
  • lockfile churn should be intentional and reviewable
  • automatic cache garbage collection means long-lived environments should not assume caches remain available forever

Once this model is stable, Cargo reproducibility becomes much easier to manage deliberately rather than by accident.