Cargo.lock is actually a guarantee that your Rust builds will be reproducible, not just a record of dependencies.
Let’s see it in action. Imagine a simple Rust project with a single dependency, rand, in Cargo.toml:
[package]
name = "my_crate"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8.5"
When you run cargo build for the first time, Cargo downloads rand and any of its sub-dependencies. It then writes the exact versions of all these crates into a file named Cargo.lock.
# Example snippet from Cargo.lock
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "..." # A long string of characters
[dependencies]
getrandom = "0.2.10" # rand 0.8.5 depends on getrandom 0.2.10
[[package]]
name = "getrandom"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "..."
Now, if you or a teammate runs cargo build on a different machine, or even on the same machine later, Cargo will ignore the version specified in Cargo.toml (rand = "0.8.5") and instead use the exact versions locked in Cargo.lock. This prevents subtle bugs that arise from different developers or CI systems pulling slightly different versions of dependencies, which might have incompatible API changes or even security vulnerabilities.
The core problem Cargo.lock solves is dependency hell, a situation where different parts of your project (or different projects on the same system) require conflicting versions of the same library. Without a lock file, cargo update or a fresh clone could pull in a new minor version of a dependency that breaks your build, even though your Cargo.toml specified a compatible range.
Here’s how it works internally: Cargo maintains a dependency graph. When you specify a dependency like rand = "0.8.5", you’re telling Cargo to find any version of rand in the 0.8.5 series (e.g., 0.8.5, 0.8.6, etc.) that satisfies all other dependency constraints. Once Cargo resolves this graph and finds a set of compatible versions for all your direct and transitive dependencies, it writes these exact versions and their checksums into Cargo.lock. The checksums ensure that the downloaded code hasn’t been tampered with and is exactly what Cargo expects.
When you commit Cargo.lock to your version control system (like Git), you’re essentially saying, "This is the definitive set of dependencies for this project at this point in time." Anyone who checks out your code and runs cargo build will get precisely these versions, leading to consistent and predictable builds across all environments.
The one thing most people don’t realize is that Cargo.lock is not just for top-level dependencies. It records the exact versions of every single crate in your entire dependency tree, including transitive dependencies. This is why updating a single direct dependency can sometimes trigger a cascade of updates and a large Cargo.lock file. It’s this comprehensive locking that provides the strongest guarantee of reproducibility.
To advance your understanding, explore how cargo update interacts with Cargo.lock and Cargo.toml when you deliberately want to upgrade dependencies.