How to upgrade Rust safely without breaking CI
CI “helpfully” upgraded Rust. Then your laptop failed the same tests.
I’ve watched teams burn a morning on ghosts that disappeared the second everyone used the same rustc. You do not need a new debugging ritual. You need one source of truth for the toolchain and one place to catch drift fast.
Safe means you can rebuild yesterday
Reproducible wins.
I mean this literally: check out a commit from last week, run cargo test, and get the same compiler and the same dependency graph. If you cannot do that, you are gambling with every “minor” Rust upgrade.
Here’s the thing. rustup will pick a toolchain from multiple places, and the winner often is not the file you just added. I’ve tripped on this in CI more than once: the repo had rust-toolchain.toml, but a stale RUSTUP_TOOLCHAIN env var still forced a different compiler.
Pin the Rust toolchain in the repo (first)
Do this first.
Add rust-toolchain.toml at the repo root so every machine reads the same instruction. I keep it boring and explicit because “whatever stable is today” does not help when you need to bisect a regression next month.
- Exact pin for serious repos: set channel = “1.75.0” (or whatever you run) so rustup selects that exact toolchain until you change the file.
- Moving channel for throwaway work: set channel = “stable” only if you accept churn. Stable moves. Your environment still needs to run updates or reinstall to pick it up.
- Keep CI lean: set profile = “minimal” so rustup does not drag docs and extra components into every runner.
Then verify it like you mean it. Run rustup show. If rustup says “active toolchain: stable,” you did not pin what you think you pinned. Read the “active because” line and follow it up the directory tree.
Make Cargo refuse the wrong compiler
Fail early.
This bit me when a teammate pulled the repo on an older laptop and Cargo tried to build until it hit a weird error deep in a dependency. Add rust-version so Cargo stops before it wastes your time.
- Put this in Cargo.toml: under [package], add rust-version = “1.75” (match your policy).
- What you get: Cargo errors out immediately on an older compiler, instead of compiling half the graph and exploding later.
In most cases, Cargo tooling also behaves better around MSRV when you set rust-version, but it depends on your resolver and your dependency mix. I have not tested every corner here, especially with older workspaces that still carry legacy settings.
MSRV testing: catch breakage before your users do
If you maintain a library, MSRV is not optional. It is a promise.
Your CI should test against both the pinned toolchain and the minimum supported Rust version. Otherwise you will ship a release that compiles on your machine and breaks on every user running the version you claimed to support.
- Add an MSRV job: run your test suite against the rust-version you declared in Cargo.toml. Use cargo +1.70.0 test (or whatever your MSRV is) as a separate CI step.
- Keep MSRV bumps deliberate: do not bump MSRV in a patch release. Treat MSRV changes as minor or major semver events. Your downstream users will thank you.
- Watch for transitive breakage: a dependency bumps its own MSRV, and suddenly your MSRV claim is a lie. Run cargo msrv verify (from cargo-msrv) to catch this automatically.
I have seen MSRV drift go unnoticed for months until a distro packager tried building with their system Rust and filed an angry issue. A two-minute CI job prevents that entire class of problem.
Lock dependencies on purpose (and know when not to)
Pinning Rust alone won’t save you.
I’ve seen builds change under the same rustc because somebody ignored Cargo.lock. Cargo wrote that file because resolution changes. For an app or service, I usually commit Cargo.lock and treat updates as a deliberate event: run cargo update, run tests, commit the diff. For a library, you might skip committing Cargo.lock so downstream users exercise wider version ranges. Some teams commit it anyway. I do not love that for libraries, but I get why they do it when CI flakiness gets old.
Never hand-edit Cargo.lock. Seriously. If you “fix” it in a text editor, you will ship a mess and you will not notice until a clean runner breaks.
Force CI to follow the same rules (no guessing)
Print the versions.
I do not trust a Rust pipeline that runs tests before it proves which compiler it used. Make CI install the toolchain from the repo file, then print versions every run. Yes, even for a patch-only change.
- Install and verify step: run rustup show, then rustc -V, then cargo -V before cargo test.
- Don’t get tricked by caching: cache downloads if you want, but still print versions so you can see drift when it happens.
If your CI image already ships with Rust, you can still do this, but you need to watch for “helpful” preinstalls that bypass rustup. That’s when version printing turns into your smoke alarm.
Automate Rust upgrades with Dependabot or Renovate
Manual upgrade reminders get ignored. Automation does not.
Both Dependabot and Renovate can detect rust-toolchain.toml and open PRs when a new Rust stable drops. This is the difference between “we upgrade when someone remembers” and “we upgrade every six weeks because the bot already ran tests.”
- Dependabot: add a cargo ecosystem entry in .github/dependabot.yml. It handles Cargo.lock updates. For toolchain file bumps, you may need a custom GitHub Action that checks the Rust release feed and opens a PR.
- Renovate: supports rust-toolchain.toml natively. Configure a rustToolchain manager in renovate.json. Renovate will bump the channel version and open a PR with the new pin.
- Group related updates: bump toolchain, clippy, and rustfmt together. Reviewing three separate PRs for the same Rust release is a waste of everyone’s time.
The key is making upgrades a boring, routine event. If upgrading Rust is a “project,” you are already behind.
Upgrade Rust on purpose (small branch, fast feedback)
Keep it small.
Create a branch. Bump the pinned channel in rust-toolchain.toml. Install it locally, run tests, then let CI confirm it sees the same toolchain. Rust stable releases land about every six weeks, so a calendar reminder beats a surprise failure on a Monday.
If something breaks, bisect it. Rust nightlies are date-stamped, so you can binary search the exact nightly that introduced a regression. Use cargo-bisect-rustc to automate this. It has saved me hours on compiler regressions that looked impossible to narrow down manually.
Other stuff you might want to do: bump clippy/rustfmt components, tweak caches, update the base image, the usual.
When rustup ignores your pin
This almost always comes from an override you forgot.
Check the obvious offenders in this order: a script that runs cargo +nightly, a RUSTUP_TOOLCHAIN env var in CI, or a directory override you set months ago and never cleaned up. Remove directory overrides with rustup override unset. Then run rustup show again and actually read the “active because” line.
If rustup disagrees with you, it is probably right.
There’s probably a cleaner checklist for this, but slowing down and reading rustup show has saved me every time.
Common mistakes that break Rust upgrades
I keep seeing the same patterns across teams. Most Rust upgrade pain comes from a handful of avoidable mistakes.
- Skipping multiple stable releases: jumping from 1.70 to 1.80 introduces ten releases of deprecations and lint changes at once. Upgrade every release or every other release. The diffs stay manageable.
- Ignoring new clippy lints: a new Rust stable often ships new clippy lints that default to warn. Your clean build suddenly has 200 warnings. Run cargo clippy as part of the upgrade PR, not as a surprise later.
- Upgrading Rust but not dependencies: some crates use features that only exist in newer Rust. If you bump Rust without running cargo update, you might miss optimizations or hit subtle compatibility edges. Do both in the same PR.
- Testing only on one OS: Rust is cross-platform, but CI runners differ. A Linux-only test suite misses Windows path handling, macOS linker quirks, and platform-specific cfg blocks. Test what you ship.
Keep Reading
- Rust releases: full version timeline and latest updates
- Rust 1.93.0 release notes: SIMD, varargs, and breaking changes
- function_casts_as_integer lint in Rust 1.93.0: How to Use
Frequently Asked Questions
How often should I upgrade Rust? Every stable release (roughly every six weeks) is the safest cadence. Each jump is small, lints and deprecations arrive in manageable batches, and you catch regressions early. Skipping multiple releases compounds the pain and makes it harder to bisect problems.
What is the difference between rust-toolchain and rust-toolchain.toml? Both pin the Rust toolchain for a project. The plain rust-toolchain file is a legacy format that only accepts a channel string. The TOML version (rust-toolchain.toml) supports additional fields like profile, targets, and components. Use the TOML version for new projects because it gives you more control over what rustup installs.
Will upgrading Rust break my Cargo.lock? Upgrading the Rust toolchain does not change Cargo.lock by itself. Cargo.lock only changes when you run cargo update or add new dependencies. However, a new compiler version may trigger different resolver behavior in edge cases, so always run tests after any toolchain change and check for lockfile diffs.
How do I roll back a bad Rust upgrade in CI? Revert the rust-toolchain.toml change. Because the toolchain is pinned in the repo, reverting the commit restores the previous compiler everywhere. If you use rustup directory overrides, remove them with rustup override unset first to make sure the file-based pin takes priority.
Can I use nightly Rust in production? You can, but you accept breakage. Nightly features can change or disappear without warning. If you depend on a nightly feature, pin to a specific nightly date (e.g. nightly-2026-01-15) rather than tracking latest. Some teams run nightly in CI alongside stable to get early warnings, then ship only stable builds.
Related Reading
- Rust 1.93.0 Release Notes — SIMD, varargs, and breaking changes
- function_casts_as_integer Lint in Rust 1.93 — How to fix the new lint
- How to Add Version Health Badges — Track release health in your README
- All Tools EOL Calendar — Track EOL dates across 30+ technologies
Step-by-step safe upgrade
Here is the exact sequence I run every time a new Rust stable drops:
# 1. Check what you are running now
rustc --version
rustup show
# 2. Update the toolchain
rustup update stable
# 3. Clean build (catches issues that incremental builds hide)
cargo clean
cargo build 2>&1 | tee /tmp/build-output.txt
# 4. Run clippy with deny-warnings (new lints appear in new versions)
cargo clippy --all-targets --all-features -- -D warnings
# 5. Run the full test suite
cargo test --all-features
# 6. If everything passes, pin the version
rustc --version > rust-toolchain-verified.txt
Pin your Rust version in CI
The most common CI breakage comes from “stable” meaning different things on different days. Pin it:
# rust-toolchain.toml (checked into your repo)
[toolchain]
channel = "1.93.0" # pin to exact version
components = ["rustfmt", "clippy"]
targets = ["x86_64-unknown-linux-gnu"]
# .github/workflows/ci.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.93.0
components: clippy, rustfmt
- run: cargo fmt --check
- run: cargo clippy -- -D warnings
- run: cargo test --all-features
Rollback plan
If the new version breaks something and you cannot fix it immediately:
# Install the previous version alongside the new one
rustup toolchain install 1.92.0
# Switch back temporarily
rustup default 1.92.0
# Or override for just this project
rustup override set 1.92.0
# Verify you are on the old version
rustc --version
Check your Rust dependencies for EOL or archived crates with the Dependency EOL Scanner. Get a full health report with the Stack Health Scorecard.
Official resources:
🛠️ Try These Free Tools
Paste your dependency file to check for end-of-life packages.
Plan your upgrade path with breaking change warnings and step-by-step guidance.
Paste your workflow YAML to audit action versions and pinning.
Track These Releases