Someone on a long-term-support Linux distribution runs your crate on a compiler that is eighteen months old. If you used a language feature from last quarter, they do not see "this crate needs Rust 1.88". They see error[E0658]: let chains are unstable pointing at a line in your code, or worse, an error in a dependency three levels down that has nothing to do with them. Declaring a minimum supported Rust version turns that confusion into a one-line message before any code compiles. This post covers the field, the message, how to choose a number, what it does to dependency resolution, and what raising it costs. Checked on Rust 1.99, edition 2024.
Declare it in the manifest
[package]
name = "msrv-demo"
version = "0.1.0"
edition = "2021"
rust-version = "1.70"That single line is the whole declaration. Cargo reads it when it builds the package, and when it resolves dependencies for anything that depends on you. If someone builds with an older compiler, Cargo stops at the start. Using a deliberately impossible value shows the message you are buying:
[package]
name = "msrv-demo"
version = "0.1.0"
edition = "2021"
rust-version = "1.999"error: rustc 1.99.0 is not supported by the following package:
[email protected] requires rustc 1.999Two things are worth noticing. The message names the package and the version it needs, which is the information the reader lacked. And the error appears whether the incompatible crate is yours or a dependency's: a dependency that declares rust-version gets the same treatment, so a project on an old compiler learns which crate raised the bar. The field must be at least as large as the highest rust-version among the dependencies you require and the highest edition feature you use; edition 2024 itself requires Rust 1.85, so a crate on that edition cannot claim anything lower.
Choosing the number
There is no correct MSRV, only a policy you can state in one sentence and keep. Common ones:
| Policy | Who uses it | Cost |
|---|---|---|
| Latest stable only | applications, internal services | none for you; excludes slow-updating users |
| Latest stable minus N releases (4 to 8) | popular libraries | you cannot use brand-new language features for a few months |
| Oldest compiler in a major distro | libraries embedded in system packages | holds back features for years |
| Fixed, bumped only at a major version | foundational crates | strictest compatibility, slowest adoption of new language |
The right row follows your audience. An application has no downstream users, so its MSRV is just the version in its Dockerfile and in rust-toolchain.toml; pinning the toolchain already says it, and a separate declaration adds little. A library has readers on every compiler, and the policy is a promise to them. Write it in the README so nobody has to guess, and apply it consistently: "we support the last six months of stable" is a sentence a user can plan around.
Find your real minimum
Do not pick a number and hope. The honest minimum is the oldest compiler that actually builds your crate, and a tool will find it by bisecting:
cargo install cargo-msrv --locked
cargo msrv find
cargo msrv verifyfind tries compiler versions, starting from the newest, and reports the oldest that succeeds. verify checks that the number in Cargo.toml still holds. Run find once to set the first declaration, and put verify or a CI job in place afterwards (testing the MSRV with cargo hack shows the pipeline). Expect the answer to be higher than the version where you wrote the code, because your dependencies' requirements count. A dependency that needs 1.81 makes your real MSRV at least 1.81 whatever your own source uses.
Let the resolver respect it
An MSRV is only useful if resolution cooperates. A fresh cargo update historically picked the newest version of every dependency, even when that version needed a newer compiler than yours, which broke MSRV builds the day a transitive dependency published. Cargo now has an MSRV-aware resolver that prefers versions compatible with the declared rust-version:
[resolver]
incompatible-rust-versions = "fallback"With fallback, the resolver chooses the newest versions that fit your MSRV when it can, and takes a newer one only when nothing older satisfies the requirement. The default for resolver version 3, which is what edition 2024 uses, already behaves this way. For older editions or a workspace that sets resolver = "2", either set the config or select the resolver explicitly:
[workspace]
members = ["crates/*"]
resolver = "3"
[workspace.package]
rust-version = "1.85"In a workspace, put rust-version in [workspace.package] and inherit it in each member with rust-version.workspace = true, so one number governs every crate. Workspaces and publishing covers the inheritance rules.
What raising it costs
An MSRV bump is a breaking change for the users stuck below the new number, and projects disagree on how to label it. Some bump the minor version (0.4 to 0.5, or 1.3 to 1.4) and consider that sufficient because the old version keeps working. Others reserve it for a major version. Whichever you choose, state it in your changelog policy, announce the bump in release notes, and do not tuck it into a patch release: a user who runs cargo update on an old compiler should not get a build failure from a patch bump. The resolver helps them, because an MSRV-aware resolve skips your new version if their compiler cannot build it, but only when the field is accurate and they have opted in.
A failing MSRV build, start to finish
Here is what an MSRV break looks like in practice. A contributor writes a let chain, stable since Rust 1.88 in edition 2024, in a crate that declares rust-version = "1.85":
pub fn first_even(items: &[u32]) -> Option<u32> {
if let Some(first) = items.first() && first % 2 == 0 {
return Some(*first);
}
None
}On a current compiler it builds, so the pull request passes review. On the declared minimum it fails with an E0658 unstable-feature error on older toolchains, and the fix is one of two decisions: rewrite the condition as a nested if let and keep the promise, or raise rust-version to 1.88 and say so in the changelog. Clippy can warn you earlier: the clippy.toml setting msrv = "1.85" makes lints that suggest newer APIs stay quiet, so a suggestion never nudges you across the line.
msrv = "1.85"Without it, clippy may recommend Option::is_none_or or other recent methods that your promise forbids. With it, the tool reads the same number you declared and checks suggestions against it.
Cases where an MSRV is not worth declaring
A binary that ships as a container image does not need one; the toolchain is in the Dockerfile. A prototype or a private tool should track stable and ignore the question. And do not declare a number you do not test: a field that says 1.70 while the code needs 1.80 produces the wrong message at the worst time, because the user trusted it. If you cannot afford the CI job, leave the field out and say "latest stable" in the README instead.



