Skip to content
rusthacks.dev

Minimum Supported Rust Version and How to Declare Yours

Declare the oldest Rust your crate supports with rust-version, test it in CI, and understand how Cargo resolves dependencies against it. Also, a policy for when to raise it.

5 min read
Summarize in AI
Flat illustration for the post “Minimum Supported Rust Version and How to Declare Yours”, composed around ownership.

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

Cargo.tomlTOML
[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:

Cargo.tomlTOML
[package]
name = "msrv-demo"
version = "0.1.0"
edition = "2021"
rust-version = "1.999"
Text
error: rustc 1.99.0 is not supported by the following package:
  [email protected] requires rustc 1.999

Two 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:

PolicyWho uses itCost
Latest stable onlyapplications, internal servicesnone for you; excludes slow-updating users
Latest stable minus N releases (4 to 8)popular librariesyou cannot use brand-new language features for a few months
Oldest compiler in a major distrolibraries embedded in system packagesholds back features for years
Fixed, bumped only at a major versionfoundational cratesstrictest 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:

Shell
cargo install cargo-msrv --locked
cargo msrv find
cargo msrv verify

find 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:

.cargo/config.tomlTOML
[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:

Cargo.tomlTOML
[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":

src/lib.rsrust
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.

clippy.tomlTOML
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.

Taggedmsrv
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.