The deadline situation
Friday afternoon. Your service is hitting a bug in serde (or tokio, or some HTTP client). You've found the issue, you have a one-line fix on a fork, and a pull request is open upstream. But the maintainer is on vacation and the merge isn't happening today. Your boss wants the fix shipped by Monday.
You have a few bad options. Vendor the dependency into your repo and edit it in place (gross, fragile). Pin to a specific commit on your fork in Cargo.toml for that one crate (works, but awkward when transitive dependencies also pull in the original crate). Wait for the upstream fix (your boss says no).
There's a fifth option, and it's the one Cargo was actually designed for. The [patch] section. Tell Cargo "everywhere this crate is used, no matter how deep in the dependency graph, use this version instead." That's exactly what [patch] does.
The shape of [patch]
[patch] lives in your Cargo.toml. It says: for the source listed (usually crates-io), replace the named crate with whatever you point at: a local path, a git repo, or a different registry. The replacement applies everywhere in the dependency graph, including indirect dependencies.
Here's the smallest example.
[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
[dependencies]
# Normal dependency. Cargo would pull serde from crates.io as usual.
serde = "1.0"
# Override: every reference to `serde` from crates.io now resolves to this local checkout.
[patch.crates-io]
serde = { path = "../my-serde" }
Run cargo build. Cargo notices the patch, reroutes serde to your local copy, and recompiles. If any of your dependencies also depends on serde, they get the patched version too, which is the whole point.
You're not changing what version your code asks for. You're changing what code that version actually resolves to. Your Cargo.toml still says serde = "1.0". Cargo's resolver still treats it as a 1.0-compatible crate. The [patch] simply substitutes the source.
Pointing at a git fork
Local paths are the easiest case. Git forks are the most common in real life. Here's the same idea but pulling from your fork:
[dependencies]
tokio = "1"
# Use my fork's bugfix branch instead of crates.io for tokio.
[patch.crates-io]
tokio = { git = "https://github.com/myname/tokio", branch = "fix-leak-in-spawn" }
cargo update -p tokio will fetch the latest from that branch. When the upstream fix lands and gets released, you remove the [patch] block and bump your tokio = "1.x" constraint to a version that contains the fix. Your code doesn't change.
You can also pin to a specific commit, which is what you want for reproducible builds:
[patch.crates-io]
tokio = { git = "https://github.com/myname/tokio", rev = "abc1234" }
Use branch while you're iterating, rev once you're shipping.
The version compatibility rule
Here's the part people miss the first time. The patched version has to be semver-compatible with the version your dependency graph asked for. If your project asks for serde = "1.0", the patched serde has to also be a 1.0.x. If you point [patch] at a serde 2.0 fork, Cargo will refuse and emit an error like:
error: failed to resolve patches for `https://github.com/rust-lang/crates.io-index`
Caused by:
patch for `serde` in `https://github.com/rust-lang/crates.io-index`
did not resolve to any crates.
or, in different conditions, a warning that the patch went unused. The fix: make sure your fork's Cargo.toml has a version number that satisfies all the constraints in the dependency graph.
This rule has a flip side. If your dependency tree asks for serde 1.0 and serde 2.0 (different ranges from different libraries), [patch] for crates-io only replaces one version family at a time. To fix this fully, you may need [patch] entries for both, or use the alternative [patch.unused]/version mechanism that Cargo offers for advanced cases.
A more realistic scenario: testing an unreleased fix
You hit a bug in reqwest. You fork it, fix it, push to your branch. Now your service can't ship until you can prove the fix works in the real environment. You don't want to publish to crates.io (that requires a name, version bumps, careful semver). You just want to test.
[package]
name = "my-service"
version = "0.3.0"
edition = "2021"
[dependencies]
# Whatever version the rest of the project agrees on.
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
# Test my fix in real conditions before opening the PR upstream.
# Once the fix is merged and a new reqwest release ships, delete this block.
[patch.crates-io]
reqwest = { git = "https://github.com/myname/reqwest", branch = "fix-redirect-loop" }
Now cargo build recompiles with your fork. You run integration tests, hit your staging environment, watch metrics. Once you're confident, you upstream the PR, and once it's released, you remove the patch block.
This is the everyday use of [patch]. Not "permanent fork" but "temporary detour while waiting for upstream."
Workspace-wide patches
In a Cargo workspace (multiple crates in one repo), [patch] belongs in the workspace Cargo.toml, not in any individual crate. That way every crate in the workspace uses the same patched version.
[workspace]
members = ["crate-a", "crate-b", "crate-c"]
# This patch applies to every member crate in the workspace.
[patch.crates-io]
serde = { git = "https://github.com/myname/serde", branch = "fix-2024-12" }
If you put [patch] inside crate-a/Cargo.toml, Cargo will warn that the patch is being ignored. The patch always belongs at the workspace root.
[patch] vs other override mechanisms
Cargo has a few related tools. They look similar and they aren't interchangeable.
[patch] rewrites a dependency wherever it's referenced in the graph. It's the right answer for "use my fork instead of upstream, even for transitive dependencies." Versions must be semver-compatible.
[replace] was an older mechanism that did something similar. It is now deprecated in favor of [patch]. If you find [replace] in old code or tutorials, treat it as obsolete.
[dependencies] foo = { path = "../foo" } overrides only your direct dependency on foo. If a transitive dep also depends on foo, it gets the crates.io version, not your local one. That's a real bug if you're trying to test a fix that lives in shared code. Use [patch] instead.
[profile.dev.package.foo] opt-level = 3 is for tweaking compilation settings of a particular crate (not source). Different problem entirely.
For tooling: cargo update and cargo tree are your friends. cargo tree -p serde shows you exactly which crates pull in serde and at which versions. That tells you what you need to patch. After editing [patch], run cargo update (sometimes targeted: cargo update -p serde) to refresh Cargo.lock.
Common pitfalls
You added a [patch] and Cargo says "patch was not used in the crate graph." That means no crate in your dependency tree actually requested the version your patch resolves to. Usually because the version in your fork's Cargo.toml doesn't match what's needed. Check cargo tree to see what versions are in play.
You added the patch in a sub-crate's Cargo.toml instead of the workspace root. The patch is ignored. Move it to the workspace Cargo.toml.
You forgot to commit Cargo.lock after the patch. Other developers (or CI) end up with a different resolution and the patch silently goes unused for them. Cargo.lock belongs in version control for binary projects.
You patched a crate but a transitive dep pinned the original via a =1.2.3 exact-version requirement. Cargo will refuse to resolve. You need to either patch in the transitive dep too or get them to relax the pin.
You're using [patch] long-term as a fork. That's not what [patch] is for. If you really need to maintain a permanent fork, publish your fork under a new name (my-org-serde) and depend on it directly. Long-running [patch] blocks confuse new contributors and hide the divergence.
When to use what
Use [patch.crates-io] when you want to test a fix or temporarily replace a crates.io dependency with your own version, including for transitive uses. The fix is in flight upstream; you don't want to wait.
Use a direct path = "../foo" dependency when you only consume the crate yourself, no transitive dependencies share it, and you're developing both crates side by side.
Use a published fork (different crate name) when you need a permanent divergence. That's a real maintenance burden but it's honest.
Don't use [patch] to silence semver warnings. If your code wants 1.0 and the new release is 2.0, the right answer is to update your code, not to patch the new version into the old slot. The compiler errors that follow are protecting you.
Where to go next
[patch] is one of those Cargo features you don't notice until the day you suddenly need it. Then it saves your week.
How to specify exact dependency versions in Rust