How to use Cargo patch for dependency overrides

Override a Rust dependency by adding a [patch.crates-io] section to your Cargo.toml file pointing to a local path or git repository.

When crates.io gets in your way

You are debugging a memory leak in a third-party HTTP client. You found the exact line causing it, wrote a fix, and pushed it to a fork. The maintainer is on vacation. Your project is blocked. You could wait weeks for a new release, or you could tell Cargo to ignore crates.io and compile your fork instead. That is exactly what the [patch] section does. It lets you surgically swap out a dependency at build time without changing your Cargo.toml dependency list.

How patching actually works

Cargo resolves your dependency graph by looking at version numbers and fetching from crates.io. When you add a [patch] block, you are giving Cargo an override map. Cargo still reads your normal dependencies, builds the graph, and then checks the patch table. If a crate in the graph matches a key in your patch section, Cargo swaps the source. The version number stays the same in the lockfile, but the actual bytes come from your local folder or git repository. Think of it like a stage manager swapping a prop mid-rehearsal. The script stays identical, but the actors interact with a different object.

Patches do not change version constraints. They only change where Cargo fetches the code. If your project asks for serde = "1.0", Cargo will still look for a 1.0.x crate. The patch just tells Cargo to grab that 1.0.x crate from a different URL instead of the registry. This design keeps the dependency graph stable while giving you full control over the source code.

Keep patches isolated to your local workflow. They are development tools, not distribution features.

Minimal example

# Cargo.toml
[dependencies]
# Normal dependency declaration stays exactly the same
serde = "1.0"

[patch.crates-io]
# Override the registry source with a local directory
serde = { path = "../my-serde-fork" }

Run cargo build. Cargo sees serde = "1.0" in your dependencies, finds serde in the patch table, and compiles from ../my-serde-fork instead. The rest of your dependency tree remains untouched. You can also point to a remote repository:

[patch.crates-io]
# Fetch a specific branch from a public or private git repo
serde = { git = "https://github.com/your-org/serde", branch = "main" }

The path and git keys are mutually exclusive. Pick one source per crate. Cargo will refuse to compile if you provide both.

What happens under the hood

The resolver performs three distinct steps when it encounters a patch. First, it builds the complete dependency graph using your normal [dependencies] and [dev-dependencies]. Second, it scans the graph for crates that match the keys in [patch.crates-io]. Third, it replaces the source URL for those matches. The version constraint in your lockfile does not change. Cargo only cares that the patched crate satisfies the original semver requirement. If your patch is version 1.0.15 and your dependency asks for ^1.0, the swap succeeds. If your patch is 2.0.0, Cargo rejects it with a version mismatch error.

Patches apply transitively. If you depend on axum, and axum depends on tokio, patching tokio will swap it for both your crate and axum. You do not need to patch every crate that uses it. The override flows down the graph automatically. This transitive behavior is what makes [patch] powerful. You fix one crate, and every downstream dependency gets the fix without manual intervention.

The lockfile plays a critical role here. Cargo.lock stores the exact source URL and commit hash for every resolved crate. When you add a patch, the lockfile becomes stale. Cargo will keep using the old registry source until you force a re-resolution. Run cargo update -p <crate-name> to tell Cargo to re-fetch and re-resolve that specific crate. If you skip this step, you will stare at your terminal wondering why your local changes are not compiling.

Trust the lockfile. It remembers what it resolved, and it will not change until you tell it to.

Realistic workflow

Real projects rarely patch a single crate with a local path. You usually need to point to a specific git branch, a commit, or patch multiple crates at once. Here is how a typical workflow looks:

[dependencies]
# Standard dependency declarations with feature flags
reqwest = { version = "0.11", features = ["json"] }
tokio = "1.28"

[patch.crates-io]
# Override reqwest with a specific feature branch
reqwest = { git = "https://github.com/you/reqwest", branch = "fix-timeout-bug" }
# Override tokio with a specific commit hash for reproducibility
tokio = { git = "https://github.com/tokio-rs/tokio", rev = "a1b2c3d" }

Notice the rev field. Branches move. Commits do not. Pinning to a commit hash guarantees that your build will compile identically tomorrow, next month, or on a CI server. The git source can also take a tag field, which is useful when you are testing a pre-release that has not hit crates.io yet.

Workspace projects handle patches slightly differently. If you are developing multiple crates in a monorepo, you can place the [patch] section in the root Cargo.toml. Every member of the workspace will inherit the override. This is the standard pattern for framework maintainers who need to test breaking changes across dozens of internal crates simultaneously.

Always pin to a commit hash when sharing patches with a team. Branches drift, and drifting branches break CI.

Pitfalls and resolution errors

The most common mistake is expecting the patch to apply instantly after editing Cargo.toml. Cargo caches resolved graphs in Cargo.lock. If the lockfile already contains a resolved version, Cargo will keep using it until you force a re-resolution. Run cargo update -p <crate-name> to tell Cargo to re-fetch and re-resolve that specific crate. If you skip this step, you will stare at your terminal wondering why your local changes are not compiling.

Another trap is version drift. Your patch must satisfy the semver range requested by your dependencies. If axum requires tokio ^1.20.0 and your patched tokio is version 1.25.0, the swap works. If your patch is 2.0.0, Cargo aborts with a resolution error. The compiler will not show a familiar E0nnn code here. Instead, you get a Cargo-level error like failed to select a version for the requirement tokio = ^1.20.0. The fix is always to bump the patch version or adjust the dependency constraint.

API breakage is a silent killer. Patches swap the source, but they do not guarantee compatibility. If your patched crate removes a public function that your code relies on, the compiler will reject you with E0432 (use of undeclared item) or E0599 (no method named found). The error looks identical to a typo in your own code. Check your patch version against the original crate's changelog before assuming the swap is safe.

Patches also break when you publish. Cargo treats [patch] as a development tool. If you run cargo publish, Cargo strips the patch section and warns you. Your crate will compile against the official crates.io version, not your local fork. This is intentional. Published crates must depend on stable, reproducible sources. Keep patches in your local workspace or a private CI configuration. Never commit a [patch] section to a public library unless you fully understand the supply chain implications.

Test your patches against the official registry before merging. If your code breaks without the patch, your dependency is too tightly coupled to your fork.

When to patch versus other approaches

Use [patch.crates-io] when you need to override a transitive dependency across your entire dependency graph. Use path dependencies when you are developing multiple crates in the same monorepo and want instant feedback without git commits. Use git dependencies when you need to test a remote branch or commit that has not been published. Use cargo update -p <name> when your changes do not appear after editing the patch section. Reach for feature flags when you only need to enable optional functionality without changing the source code. Trust the borrow checker. It usually has a point.

Where to go next