How to Use rust-toolchain.toml to Pin Toolchain Versions

Pin your Rust toolchain version by creating a rust-toolchain.toml file with the desired channel and components.

The "It Works on My Machine" Killer

You merge a feature branch. The build passes on your laptop. CI fails with a cryptic error about a missing trait or a syntax change. Your teammate on a different OS gets a different error. The culprit isn't your code. It's the compiler version. One of you is on Rust 1.75, another on 1.78. The language evolved between those releases, and your code sits in the gap.

This happens every week in Rust projects that skip toolchain pinning. The fix is a single file. rust-toolchain.toml declares the compiler version for a project. It lives in the project root. It forces rustup to use a specific toolchain. rustup is the manager that installs and switches Rust versions. The file makes the switch automatic. No manual setup. No environment variables to remember. No "works on my machine" excuses.

What the file does

rust-toolchain.toml is a contract between your project and the compiler. It tells rustup exactly which version of Rust to use when anyone runs a command inside the project directory. When you run cargo build, rustup reads this file, installs the specified version if it is missing, and switches to it transparently.

The file uses TOML format. It has one main section: [toolchain]. Inside that section, you specify the channel, which is the version. You can also list components like clippy or rustfmt, and targets for cross-compilation.

[toolchain]
# Pin to a specific patch version for reproducible builds.
channel = "1.78.0"

# List tools that your build process or CI requires.
# If you omit these, rustup might not install them automatically.
components = ["clippy", "rustfmt"]

# Add targets if you compile for other architectures.
# This ensures the standard library for that target is installed.
targets = ["wasm32-unknown-unknown"]

Commit this file to version control. If it is not in git, it does not exist for your team.

How the switch happens

Rust commands like cargo and rustc are not the real binaries. They are shims provided by rustup. When you type cargo build, the shell finds cargo in your PATH. That cargo is a small wrapper script. The wrapper checks the current directory for rust-toolchain.toml.

If the file exists, the shim parses the channel. It checks rustup's registry of installed toolchains. If the requested version is missing, the shim downloads and installs it. Then it sets up environment variables pointing to the bin directory of that toolchain and executes the real cargo. The switch is invisible. You never see the download unless it is the first time.

This mechanism works for every command. cargo test, cargo doc, rustfmt, clippy. They all go through the shim. They all respect the file. The toolchain switches based on the directory, not your global default.

Realistic configuration

A real project often needs more than just the compiler. CI scripts might run clippy for linting. Developers might run cargo fmt. You might build for WebAssembly. The toolchain file can request all of these.

[toolchain]
# Pin to the exact version used in development.
# Patch versions include bug fixes and security updates.
channel = "1.78.0"

# CI pipelines often fail if components are missing.
# Explicitly listing them ensures rustup installs them on fresh runners.
components = ["clippy", "rustfmt", "rust-docs"]

# If the project builds for WebAssembly, add the target.
# Without this, cross-compilation fails with a missing target error.
targets = ["wasm32-unknown-unknown"]

Convention aside: list components that your project relies on. If your Makefile runs cargo clippy, add clippy to the list. If you skip it, a fresh clone might fail because rustup only installs the compiler by default. The community considers it good practice to declare dependencies on tools in the toolchain file.

Pitfalls and gotchas

Nightly drift

Using channel = "nightly" is dangerous. Nightly changes every day. A build that works today might fail tomorrow because a nightly regression broke a feature. Pin the date instead.

[toolchain]
# Pin nightly to a specific date to avoid daily breakage.
channel = "nightly-2024-05-20"

If you use a feature not yet stabilized, the compiler emits E0658. Pinning the date ensures you always have the version where that feature exists.

Legacy files

Older projects might have a rust-toolchain file. This is a plain text file with just the version string. rust-toolchain.toml overrides it. If you have both, the TOML file wins. Delete the legacy file to avoid confusion. The community has moved to TOML for its flexibility.

Workspace roots

In a workspace with multiple crates, put rust-toolchain.toml in the workspace root. It applies to all member crates. Do not put it in individual crates. If you do, rustup might switch toolchains mid-build, causing conflicts. One file per workspace.

Override hierarchy

rustup checks multiple sources for the toolchain version. The priority is:

  1. rust-toolchain.toml in the current directory.
  2. rust-toolchain.toml in parent directories.
  3. rustup override set for the directory.
  4. The default toolchain.

The file in the current directory takes precedence. This means you can have a workspace-wide toolchain and override it for a specific subdirectory if needed. Use this sparingly. It complicates the setup.

CI caching

If you change the toolchain version, your CI cache might still have artifacts from the old version. Clear the cache or key it on the toolchain file hash. GitHub Actions rust-lang/setup-rust reads the file automatically and handles caching correctly. If you write custom CI scripts, ensure you invalidate caches when the toolchain changes.

Decision matrix

Use rust-toolchain.toml when you want the toolchain to be part of the project configuration and shared by all contributors. Use channel = "1.78.0" when you need reproducible builds across time and machines. Use channel = "stable" only for throwaway scripts where reproducibility does not matter. Use channel = "nightly-YYYY-MM-DD" when you need nightly features but want to avoid daily breakage. Use rustup override when you are experimenting locally and do not want to commit a change. Use components when your build process or CI relies on tools like clippy or rustfmt. Use targets when you cross-compile for architectures other than the host.

Pin the patch version. Reproducibility matters.

Where to go next