How to Manage the MSRV (Minimum Supported Rust Version)

Set the MSRV in Cargo.toml to guarantee your Rust crate compiles on older compiler versions.

The broken build that shouldn't happen

You publish a new version of your library. The tests pass on your machine. CI turns green. A week later, a downstream project tries to add your crate to their Cargo.toml and their build explodes. They are running Rust 1.68. You accidentally used a method that landed in 1.71. Their compiler throws a wall of red text. They open an issue. You realize you never told anyone what compiler version your code actually needs.

This happens because Rust moves fast. New syntax, new standard library methods, and new compiler diagnostics land every six weeks. Without a declared floor, your crate implicitly requires whatever version you happened to be running when you wrote the last commit. That implicit requirement breaks user workflows and makes dependency resolution a guessing game.

What MSRV actually does

MSRV stands for Minimum Supported Rust Version. It is a contract between you and everyone who compiles your code. The contract says your crate will build successfully on any stable Rust release at or above the stated version, and it will not build on versions below it.

Think of it like a minimum engine displacement for a vehicle. A car designed for a 2.0L engine will not run properly on a 1.2L. The manufacturer prints the requirement on the spec sheet so buyers know what they are getting into. Your rust-version field does the same thing for compilers. It tells package managers, CI systems, and human developers exactly where the compatibility line sits.

Setting the floor also protects you from your own future self. When you upgrade your local toolchain to the latest stable release, you will inevitably start using new language features. The MSRV field acts as a guardrail. Cargo will refuse to compile your code if you accidentally rely on syntax or library methods that did not exist in your declared minimum.

Declare the floor early. Change it deliberately. Never let it drift.

Declaring your floor

The official way to declare your MSRV is the rust-version field in your Cargo.toml. It lives under the [package] section alongside name and version.

[package]
name = "example-crate"
version = "0.1.0"
edition = "2021"
rust-version = "1.70.0"

The value must be a valid stable Rust version. Cargo reads this field during dependency resolution and compilation. It does not automatically install the version for you. It only enforces the boundary.

You should set this field the moment you create the crate. Do not wait until you have a release candidate. The default behavior without this field is effectively "whatever version you are running right now," which is a moving target.

/// Verify the declared floor matches your actual requirements.
/// Run this check before every release to catch accidental drift.
fn main() {
    // Cargo reads rust-version from Cargo.toml automatically.
    // No runtime code is needed to enforce it.
    // The compiler handles the contract at build time.
    println!("MSRV enforcement is a compile-time guarantee.");
}

Convention aside: the Rust community expects rust-version to match the oldest stable release that successfully builds the crate. Do not pad it with extra minor versions unless you have a specific reason. If your code builds on 1.70.0, declare 1.70.0. Users will trust the number.

How Cargo enforces the contract

When you run cargo build or cargo check, Cargo compares the compiler version you are using against the rust-version field. If your active rustc is older than the declared floor, Cargo aborts immediately. It prints a clear message telling you to upgrade your toolchain.

The enforcement happens before any of your code is compiled. This means you do not need custom scripts or pre-build hooks. The package manager handles it natively.

# Check if your current toolchain meets the declared floor.
# Cargo will fail fast if rustc is older than rust-version.
cargo check

You can also test against a specific older toolchain without changing your default environment. rustup makes this straightforward. You pin a version, run the check, and verify the build succeeds.

# Install the exact floor version if you do not have it yet.
rustup install 1.70.0

# Run the compiler against that specific toolchain.
# The +version syntax tells rustup to use that toolchain for this command only.
cargo +1.70.0 check

This workflow catches accidental drift before it reaches users. If you add a new dependency that requires 1.72.0, your check against 1.70.0 will fail. You then have a choice: bump your rust-version or find a compatible dependency version.

Let the compiler tell you when the floor shifts. Do not guess.

Finding the real floor

Setting a number in Cargo.toml is only half the job. The harder part is discovering what that number actually is. Your crate might depend on five other libraries, each with their own MSRV. The effective floor for your project is the highest MSRV in your dependency graph.

You could manually test older versions, but that is tedious and error-prone. The ecosystem provides a dedicated tool for this: cargo-msrv. It performs a binary search across stable Rust releases to find the lowest version that successfully compiles your crate.

# Install the community tool for MSRV discovery.
cargo install cargo-msrv

# Run the search against your current dependency tree.
# It will test versions until it finds the lowest working one.
cargo msrv find

The tool outputs the exact version that works. You copy that number into rust-version. You then verify it by running cargo +<found-version> check. If the check passes, your declared floor matches reality.

[package]
name = "example-crate"
version = "0.1.0"
edition = "2021"
# Updated after running cargo msrv find
rust-version = "1.72.0"

Convention aside: run cargo msrv find after every significant dependency update. A single cargo update can pull in a newer version of a transitive dependency that raises the floor. Your rust-version field will silently become inaccurate until you verify it again.

Treat MSRV discovery as a routine maintenance task. Automate it in CI if your project grows beyond a single developer.

Where things go sideways

The most common failure mode is dependency mismatch. You declare rust-version = "1.70.0", but one of your dependencies requires 1.73.0. Cargo will refuse to resolve the dependency graph for anyone running 1.70.0 through 1.72.0. Their build fails with a resolution error, not a compiler error. The error message points to the dependency, not your crate, which confuses users.

error: failed to select a version for `some-dep`.
... required by package `example-crate v0.1.0`
... version `1.73.0` is the minimum compatible version

You fix this by bumping your rust-version to match the dependency requirement, or by pinning the dependency to an older version that supports your floor. You cannot declare a floor lower than your heaviest dependency.

Another trap is unstable features. If you accidentally use a feature that is still behind a feature gate, the compiler rejects it with E0658 (unstable feature). This happens regardless of your MSRV setting. The compiler enforces stability boundaries separately from version boundaries. You must keep your code on stable-only APIs if you want your crate to compile on any stable release.

// This will fail on stable Rust with E0658.
// Feature gates are not part of the MSRV contract.
// They are a separate stability boundary.
#![feature(unstable_trait)]

A third pitfall is forgetting to update the field after adding new syntax. You upgrade your local Rust to 1.75.0. You start using a new standard library method. You forget to bump rust-version. Users on 1.70.0 get a cryptic "method not found" error. The compiler does not automatically update your Cargo.toml. You are responsible for keeping the contract accurate.

Run cargo +<floor> check before every release. If it fails, your floor is wrong. Fix it before you publish.

Choosing your strategy

Use rust-version in Cargo.toml when you want Cargo to enforce a compile-time boundary automatically. Use cargo msrv find when you need to discover the actual lowest working version across your dependency graph. Use cargo +<version> check in CI when you want to verify that your declared floor still compiles after dependency updates. Use a matrix build in CI when you need to test multiple older toolchains to catch edge cases or platform-specific regressions. Reach for rustup override in your project directory when you want to lock your local development environment to the floor version, ensuring you never accidentally write code that exceeds it.

Where to go next