How to Downgrade a Dependency in Cargo

Downgrade a Cargo dependency by setting a specific older version number in Cargo.toml and running cargo update.

When the new version breaks your build

You run cargo update to keep your project current. The build succeeds, but your tests start failing. A dependency changed its API in a way that breaks your code, or it introduced a regression that crashes your application under load. You need to roll back that one library to a version that works, without reverting your entire project or touching dependencies that are fine.

Cargo makes this possible, but it requires understanding how version resolution works. Changing a number in Cargo.toml is only half the battle. You also need to update the lockfile and verify that transitive dependencies don't block the downgrade.

The wishlist and the receipt

Cargo manages versions through two files. Cargo.toml is your wishlist. You write down what you want, usually using ranges like serde = "1.0". Cargo.lock is the receipt. It records exactly what Cargo resolved and downloaded for your project.

When you downgrade, you are changing the wishlist and forcing Cargo to rewrite the receipt. If you only change Cargo.toml and run cargo build, Cargo might ignore your change. The lockfile takes precedence for reproducible builds. Cargo sees the lockfile says 1.0.195 and keeps using it, even if Cargo.toml now mentions 1.0.150. You must explicitly tell Cargo to update the lockfile.

Minimal downgrade

To downgrade a dependency, specify the older version in Cargo.toml and run cargo update targeting that package.

[dependencies]
# Pin to 1.0.150 to avoid a regression introduced in 1.0.195
serde = "1.0.150"
# Update only serde in the lockfile, leaving other dependencies untouched
cargo update -p serde

The -p flag is essential. Running cargo update without -p updates every dependency to the latest compatible version. That defeats the purpose of a targeted downgrade and can introduce new breakage elsewhere. Always scope the update to the package you are changing.

What happens under the hood

When you run cargo update -p serde, Cargo performs a resolution pass. It reads Cargo.toml and sees the constraint 1.0.150. It interprets this as ^1.0.150, which means >=1.0.150 and <2.0.0. Cargo checks the registry index for available versions. It finds 1.0.150 is available.

Next, Cargo checks compatibility. It looks at every other dependency in your project. If another crate requires serde >= 1.0.190, Cargo cannot downgrade to 1.0.150. The resolver fails with a conflict error. If no other crate blocks the downgrade, Cargo writes 1.0.150 to Cargo.lock and fetches the crate if it is not already cached.

The resolution is greedy. Cargo picks the highest version that satisfies all constraints. If you specify 1.0.150 in Cargo.toml, Cargo might still pick 1.0.195 if the lockfile already contains it and you haven't run cargo update. The update command forces the re-evaluation.

Realistic workflow with precise pinning

Editing Cargo.toml is the standard approach for permanent downgrades. For debugging or temporary fixes, the community prefers a different tool. The --precise flag lets you downgrade the lockfile without touching source files.

# Downgrade serde to exactly 1.0.150 in the lockfile only
cargo update -p serde --precise 1.0.150

This command updates Cargo.lock directly. Cargo.toml remains unchanged. This is useful when you are investigating a CI failure, testing a hypothesis about a bug, or working in a shared repository where you don't want to commit a version change yet.

Convention aside: Use --precise for transient investigations. If the downgrade fixes a real issue, commit the change to Cargo.toml as well. A lockfile-only change is invisible to new clones and can confuse teammates who run cargo update.

Inspecting transitive dependencies

Downgrading a direct dependency can have ripple effects. Your crate might depend on reqwest, which depends on hyper. If you downgrade reqwest, you might pull in an older version of hyper. Or worse, another crate might depend on hyper directly and require a newer version, blocking the downgrade.

Use cargo tree to inspect the dependency graph.

# Show the dependency tree for reqwest
cargo tree -p reqwest

# Find all crates that depend on serde
cargo tree -i serde

The -i flag inverts the tree. It shows which crates pull in serde. If you see a crate you don't control requiring serde >= 1.0.190, that crate is blocking your downgrade. You cannot downgrade serde past that point without also downgrading the blocker or using a patch.

The exact version trap

A common mistake is assuming that writing a full version number pins the dependency. It does not.

[dependencies]
# This is NOT a pin. It means ^1.0.150.
serde = "1.0.150"

Cargo interprets 1.0.150 as ^1.0.150. This allows 1.0.151, 1.0.195, or any patch release up to 2.0.0. If you want to lock to exactly one version, you must use the = prefix.

[dependencies]
# This pins to exactly 1.0.150. No updates allowed.
serde = "=1.0.150"

Pinning with = is rare in production code. It prevents security patches and bug fixes from flowing in automatically. Use = only when you have a specific reason to block all updates, such as a known vulnerability in newer versions that has no workaround. For most downgrades, specifying the older version and running cargo update is sufficient. Cargo will pick the highest version that satisfies your constraint, which is usually what you want.

Pitfalls and compiler errors

Downgrades can trigger compile errors if the older version removed APIs you rely on. The compiler will reject your code with E0432 (use of undeclared crate or module) if a struct or trait disappeared. Or you might see E0599 (no method named ... found for struct ...) if a method was renamed or removed.

error[E0432]: unresolved import `serde::de::IgnoredAny`
  --> src/lib.rs:2:5
   |
2  | use serde::de::IgnoredAny;
   |     ^^^^^^^^^^^^^^^^^^^^^ no `IgnoredAny` in `de`

Check the changelog of the library before downgrading. Many crates document breaking changes between minor versions. If you downgrade across a minor version boundary, expect API differences.

Another pitfall is version resolution conflicts. If your project has two dependencies that require incompatible versions of a third crate, Cargo cannot resolve the graph. You will see an error like failed to select a version for the requirement .... This usually means you need to downgrade both conflicting dependencies, or use a [patch] section to override the transitive dependency.

Transitive dependencies can also block downgrades silently. You might downgrade A, but B still pulls in the new version of A because B requires it. Cargo keeps the newer version to satisfy B. Your code still uses the new version, and the bug persists. Always verify the downgrade with cargo tree or by checking Cargo.lock after the update.

Decision matrix

Use Cargo.toml version pinning when you need a permanent downgrade for the project's stability and want the change to be visible in source control. Use cargo update -p --precise when you need a quick temporary rollback for debugging or CI without modifying source files. Use [patch] sections when you need to override a transitive dependency that your direct dependencies do not control. Use cargo tree -i when you suspect a transitive dependency is blocking your downgrade and need to find the culprit. Use exact version pinning with = only when you must block all updates due to a critical regression or vulnerability.

Don't fight the resolver. If Cargo cannot downgrade, a dependency somewhere is holding the line. Find the blocker, downgrade it, or patch it.

Where to go next