How to Update Dependencies in Rust with Cargo

Run `cargo update` to refresh the `Cargo.lock` file with the latest compatible versions of your dependencies, or use `cargo update -p <package>` to target a specific crate.

The lock file is your anchor

You've been hacking on a Rust project for a few weeks. The code compiles. The tests pass. You go to deploy, and a security alert pops up: a dependency has a vulnerability. You need the fix, but you're hesitant. You remember dependency hell from other languages, where one update breaks half your codebase. You want the patch, but you don't want to gamble on a full tree rewrite.

Rust handles this with a split approach. You define what you need, and Cargo records exactly what you got. This keeps you in control while guaranteeing that your build is reproducible. You update dependencies by refreshing the record, not by guessing.

Constraints versus pins

Rust splits dependency management into two files. Cargo.toml holds your constraints. Cargo.lock holds the pins.

Think of Cargo.toml as a recipe. It says, "I need 2 eggs." It doesn't specify the farm, the batch number, or the exact weight. It just says eggs. In Rust terms, serde = "1.0" means "any version compatible with 1.0." Cargo interprets this as a range. By default, 1.0 is sugar for ^1.0, which allows 1.1, 1.42, or 1.999, but blocks 2.0. The caret means compatible. Major version bumps are considered breaking, so they require an explicit change.

Cargo.lock is the receipt from the grocery run. It says, "I bought 2 eggs from Farm A, batch #12345, weighing exactly 56 grams." It records the exact version of every crate in your tree, including transitive dependencies. When you share your code, the lock file ensures everyone builds with the same bits. If you commit the lock file, your teammate gets the exact same dependency graph, down to the patch level.

cargo update is the tool that refreshes the receipt. It looks at your constraints in Cargo.toml, checks the registry for newer versions that fit, and writes a new lock file. It never changes Cargo.toml on its own. You control the ranges; Cargo controls the resolution.

Minimal example

Start with a simple constraint. You need reqwest, and you're happy with any 1.x version.

# Cargo.toml
# Define the dependency range.
# Cargo interprets "1.0" as "^1.0", meaning any 1.x version.
[dependencies]
reqwest = "1.0"

Run the update command to refresh the lock file.

# Update the lock file to the newest versions within constraints.
# This command reads Cargo.toml and writes Cargo.lock.
# It never modifies Cargo.toml.
cargo update

Cargo talks to crates.io. It finds the latest reqwest that satisfies >=1.0, <2.0. If reqwest 1.42.0 exists, Cargo writes that into Cargo.lock. If reqwest 2.0 exists, Cargo ignores it because 2.0 breaks the constraint. The lock file moves forward, but only as far as your constraints allow.

How resolution works

When you run cargo update, Cargo doesn't just bump the crate you asked for. It resolves the entire dependency tree.

Cargo reads Cargo.lock to see what's currently pinned. It then queries the registry for updates. For each dependency, it checks if a newer version exists that satisfies the constraints from all parents. If reqwest depends on hyper, and hyper has a new version, Cargo might bump hyper too. This keeps the tree consistent. You can't have reqwest 1.42 talking to hyper 0.14 if reqwest requires hyper 1.0. Cargo ensures every link in the chain is valid.

This is why a single update can ripple through the lock file. It's not magic; it's consistency. Cargo guarantees that the resolved graph is coherent. If you update one crate, Cargo checks whether transitive dependencies can also move forward without violating any constraints.

Realistic workflows

In practice, you rarely update everything at once. A full tree refresh can introduce too many changes to review. You usually target specific crates or handle major version bumps.

Updating a single package

Use the -p flag to update a specific crate. This limits the scope of the change.

# Update a specific package and its transitive dependencies.
# Other packages remain pinned to their locked versions.
# Use this to isolate changes and reduce merge conflicts.
cargo update -p serde

This command updates serde to the newest version allowed by your constraint. It also updates serde's dependencies if they have compatible updates. Other crates in your tree stay exactly where they are. This is the standard way to apply security patches or test a new feature in isolation.

Handling major version bumps

When a crate releases a major version, your constraint blocks it. You must edit Cargo.toml first.

# Cargo.toml
# Old constraint blocked 2.0.
# serde = "1.0"

# New constraint allows 2.0.
serde = "2.0"

After widening the constraint, run the update.

# Now cargo update can pull in serde 2.0.
# Cargo resolves the tree against the new constraint.
cargo update

Major versions often bring breaking changes. Your code might fail to compile. The compiler will reject you with E0432 (unresolved import) if a function moved, or E0277 (trait bound not satisfied) if a type lost a trait. Fix the code to match the new API, then commit the updated lock file. Don't merge a major version bump until the build passes.

Forcing a re-resolution

Sometimes the lock file gets stale in subtle ways. Transitive dependencies might have updates that your direct dependencies allow, but the lock file hasn't picked them up because Cargo prefers stability. Use the aggressive flag to force a full re-resolution.

# Force a full re-resolution of the dependency tree.
# This ignores the current lock file for transitive dependencies.
# Useful when transitive crates are stale but direct constraints haven't changed.
cargo update --aggressive

This tells Cargo to pretend the lock file doesn't exist for resolution purposes. It recomputes the entire graph from scratch, pulling the latest versions for everything that fits your constraints. Use this when you suspect the lock file is stuck or when you want to maximize freshness across the whole tree.

Pitfalls and conventions

No changes detected

If you run cargo update and see no changes, your lock file is already up to date for your constraints. Cargo won't bump versions just because you asked; it respects the ranges. If you need a newer version, check your constraint. You might be blocked by a narrow range, or the registry might not have a newer version yet.

Breaking changes in CI

Never run cargo update inside a CI build pipeline. CI should verify the lock file works. Run updates in a dedicated dependency-update job or locally, then commit the result. If CI runs updates, you lose reproducibility. Two builds of the same commit could pull different dependencies. The community convention is clear: binaries commit Cargo.lock; libraries do not. If you're writing a crate for others to use, let their resolver handle the lock file. If you're building an application, commit the lock file to guarantee reproducible builds.

Transitive updates surprise you

Updating serde might update log or tracing. This happens because serde or one of its dependencies depends on those crates. Cargo updates the whole subtree to maintain consistency. If you want to avoid this, you can pin transitive dependencies in Cargo.toml, but that's rare and usually fights the resolver. Trust the tree. If Cargo updates a transitive dep, it's because the new version is compatible with everything else.

Checking before updating

Use cargo outdated to see what's available without modifying files. This tool isn't built into Cargo, but it's a community standard.

# Install the helper tool if you don't have it.
# cargo install cargo-outdated

# List outdated dependencies.
# Shows current version, latest version, and whether an update is possible.
cargo outdated

This gives you a clear overview. You can see which crates have major version bumps, which have patches, and which are already current. Use this to plan your updates.

Decision matrix

Use cargo update when you want to refresh the entire dependency tree to the latest versions allowed by your Cargo.toml constraints. Use cargo update -p <name> when you need to bump a single crate without risking a full tree re-resolution. Use cargo update --aggressive when you want to force a full re-resolution of all dependencies, ignoring the current lock file state for transitive crates. Edit Cargo.toml when you need to accept a major version bump that breaks semver compatibility. Use cargo outdated when you want a report of available updates without modifying any files.

Update constraints before versions. If the constraint blocks the version, the command won't work. Check the lock file in version control. If it changes, review the diff. A broken build is worse than an outdated dependency.

Where to go next