What Is Cargo.lock and Should I Commit It?

Commit Cargo.lock to your repository to ensure reproducible builds by locking dependency versions, unless you are developing a library crate.

The "Works on My Machine" Mystery

You push a commit. Continuous integration passes. A teammate pulls the branch, runs the project, and watches it crash on startup. You both have the exact same Cargo.toml. The difference lives in a file neither of you looked at.

This happens when dependency versions drift. Rust's package manager, Cargo, follows semantic versioning. When you write serde = "1.0" in your manifest, you are not asking for version 1.0.0. You are asking for any 1.x release that is compatible. If a new 1.0.199 drops while your teammate is cloning the repo, Cargo will happily grab it. If that release changes a default behavior, breaks an internal macro, or pulls in a different transitive dependency, your build diverges. The lock file exists to stop that divergence before it reaches production.

What Cargo.lock Actually Does

Cargo.toml declares your intent. Cargo.lock records the reality.

Think of a restaurant kitchen. The menu says "organic chicken breast." That is your Cargo.toml. It gives the chef flexibility to source from different farms as long as the product meets the standard. The supplier invoice, however, lists the exact farm, the batch number, the delivery date, and the weight. That invoice is your Cargo.lock. It guarantees that every plate served tonight matches the plate served last night, regardless of which supplier truck pulled into the back alley.

The lock file is a TOML document generated by Cargo. It contains the exact version, source URL, and checksum for every single crate in your dependency tree. This includes crates you explicitly requested and crates those crates depend on. It also records the exact feature flags enabled for each dependency. When you run cargo build, Cargo reads the lock file first. If it exists, Cargo installs exactly those versions. If it does not exist, Cargo resolves the tree from scratch and writes a new one.

Treat Cargo.toml as your intent and Cargo.lock as your reality. Never swap them.

How the Lock File Gets Built

Cargo's dependency resolver works backward from your project. It fetches the registry index, finds the highest version that satisfies your semver range, then repeats the process for that crate's dependencies. It continues until every node in the graph has a version attached. The resolver prefers newer versions to keep your project close to the bleeding edge, but it respects your bounds.

[package]
name = "my-cli-app"
version = "0.1.0"
edition = "2021"

[dependencies]
# Request any 1.x version of serde. Cargo will pick the highest compatible release.
serde = "1.0"
# Request a specific minor version. Cargo will stay within 2.3.x.
tokio = "2.3"

When you run cargo build on a fresh clone, Cargo contacts the crates.io index. It downloads metadata for serde 1.0.203, checks its dependencies, resolves proc-macro2, quote, and syn, and repeats until the graph is complete. It writes the result to Cargo.lock. The next time you run cargo build, Cargo skips the registry entirely. It reads the lock file, downloads the exact tarballs, and compiles. Build times drop. Network requests disappear. The output becomes deterministic.

Let Cargo do the heavy lifting. The lock file is just the snapshot of that work.

The Binary vs Library Divide

The question of whether to commit the lock file splits cleanly along one line: are you shipping an application or publishing a library?

Binary crates are end products. They run on servers, ship as CLI tools, or execute in CI pipelines. Every deployment must be identical. Security patches, subtle behavior changes, and compilation times all depend on exact versions. If your production server runs serde 1.0.198 but your staging environment pulls 1.0.201, you introduce a variable you cannot debug. Committing the lock file freezes the dependency tree. It guarantees that cargo build produces the same binary on your laptop, your teammate's machine, and your deployment server.

Library crates are building blocks. They get published to Crates.io and pulled into other projects. Each consumer has their own dependency tree and their own version requirements. If you commit your lock file to a library, you force your specific dependency versions onto everyone who depends on you. This causes diamond dependency conflicts. Project A depends on your library and reqwest 0.11. Your lock file pins reqwest 0.10. Cargo cannot satisfy both. The consumer's build fails. Libraries must float. They declare compatible ranges in Cargo.toml and let each downstream project resolve the final graph.

Ship binaries with a lock file. Ship libraries with just a manifest.

Real-World Workflow and Pitfalls

Teams that ignore the lock file eventually pay for it in debugging time. The standard workflow treats the lock file as a first-class artifact. You commit it alongside your source code. You review changes to it in pull requests. You use it to enforce consistency across environments.

# Generate the lock file if it is missing.
cargo generate-lockfile

# Update a single dependency to its latest compatible version.
# This keeps the rest of the tree stable while testing one change.
cargo update -p serde

# Build strictly using the committed lock file.
# Fails immediately if the local lock file differs from git.
cargo build --locked

The --locked flag is a community convention for CI pipelines. It tells Cargo to refuse building if the lock file on disk does not match the one in version control. This catches accidental cargo update runs on developer machines before they reach the merge queue. It also prevents silent drift when someone clones the repo on a machine with an outdated Cargo version.

Manual editing of Cargo.lock is a common trap. The file is auto-generated. If you open it in a text editor and change a version number, Cargo will overwrite your changes on the next build. The resolver does not read your hand edits. It recalculates the graph and writes a new file. Use cargo update to change versions. Use cargo update -p crate_name to target specific crates. Use cargo update -p crate_name --precise 1.2.3 to pin to an exact release.

Never hand-edit the lock file. Let Cargo manage the graph, you manage the versions.

When to Commit and When to Ignore

The decision follows a strict pattern. Match your project type to the correct workflow.

Commit Cargo.lock when you are building a binary crate that will be deployed to servers, shipped as a CLI tool, or run in CI pipelines.

Commit Cargo.lock when your project includes integration tests that depend on exact dependency behavior or specific feature flag combinations.

Commit Cargo.lock when you are maintaining a monorepo with multiple binary targets and need a single source of truth for the entire workspace.

Ignore Cargo.lock when you are publishing a library to Crates.io or any package registry.

Ignore Cargo.lock when you are experimenting with a new dependency and want Cargo to automatically resolve the latest compatible versions without manual intervention.

Ignore Cargo.lock when your project is a pure documentation site or a collection of example snippets that do not compile into distributable artifacts.

Follow the pattern. Binaries lock. Libraries float.

Where to go next