Build Profiles (dev, release)
How Cargo's dev and release profiles control compiler settings and how to customize them for your project
Every time you run cargo build or cargo build --release, Cargo passes a carefully chosen set of flags to the Rust compiler. These flags are bundled into named build profiles. The two most important profiles are dev (used during everyday development) and release (used when you want an optimized binary for distribution or benchmarking).
Build profiles exist so you don't have to memorise dozens of compiler flags. They give you a single lever to toggle between "compile quickly" and "run quickly"—and they let you fine‑tune the trade‑offs when the defaults aren't quite right.
Which profile is used when
Cargo selects a profile automatically based on the command you run, unless you override it explicitly.
| Command | Default profile |
|---|---|
cargo build | dev |
cargo run | dev |
cargo test | test (inherits from dev) |
cargo build --release | release |
cargo install | release |
You can see which profile was used in the build output:
$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.15s
$ cargo build --release
Finished release [optimized] target(s) in 2.30s
The words dev and release are the profile names, and the bracketed text gives a hint about the compiler flags that profile activated.
Other profiles:
Cargo also defines test and bench profiles. They inherit settings from dev and release respectively, then add their own adjustments (like enabling debug assertions in tests). This page focuses on dev and release because they are the foundation every Rust project uses.
The default settings
Cargo ships with a set of defaults that are sensible for most projects. The table below shows the key settings for the dev and release profiles as of Rust 1.83.
| Setting | dev default | release default | What it does |
|---|---|---|---|
opt-level | 0 | 3 | Optimisation level for generated code |
debug | true (full debuginfo) | false | Amount of debug information |
debug-assertions | true | false | Whether debug_assert! and overflow checks are active |
overflow-checks | true | false | Runtime checks for integer overflow |
lto | false | false | Link‑time optimisation |
codegen-units | 256 | 16 | How many parallel compilation units |
panic | 'unwind' | 'unwind' | What happens on panic (unwind or abort) |
incremental | true | false | Whether to reuse previous compilation data |
strip | "none" | "none" | Whether to remove symbols or debuginfo from the binary |
rpath | false | false | Whether to embed runtime library search paths |
Every one of these defaults can be changed. You are not stuck with the trade‑offs Cargo chose.
Settings in dependency Cargo.toml files are ignored:
If you add [profile.release] inside a library you depend on, it will have no effect on the final binary. Cargo only reads profile settings from the top‑level package (the one you are building directly) or from Cargo configuration files.
What each setting controls
Understanding what the knobs actually do helps you make informed decisions instead of blindly copying snippets.
opt-level – how aggressively the compiler optimises
This setting controls the -C opt-level flag passed to rustc. It determines how much time the compiler spends analysing and transforming your code to make it run faster or use less memory.
0– no optimisation. Fast compilation, but the resulting binary runs slower.1– basic optimisations that don’t add too much compile time.2– a solid middle ground for releases.3– maximum optimisation. This is the default forrelease."s"– optimise for binary size."z"– like"s", but also disables loop vectorisation for even smaller output.
The relationship is not always linear. Level 2 sometimes produces faster code than 3 for specific programs, and "s" might not create a smaller binary than "z". The only way to be sure is to measure with your own project.
debug – how much debug information is kept
Controls -C debuginfo. In dev, the default is true (which maps to 2, full debug information). In release, the default is false (no debug info).
Intermediate options exist for cases where you want backtraces with line numbers but don’t need variable inspection:
"none"or0– no debug info at all."line-tables-only"– file names and line numbers for backtraces."limited"or1– module‑level information without per‑variable details."full"or2– everything, required for a full debugger experience.
If you ship a release binary but still want meaningful panic backtraces, setting debug = "line-tables-only" is often a good middle ground—it adds very little to the binary size.
lto – link‑time optimisation
Link‑time optimisation lets LLVM see your entire program at once, across crate boundaries, and optimise accordingly. The default false means "thin local LTO"—a light version that only works inside the local crate. The more powerful options are:
"thin"– Thin LTO. Good performance gains with manageable link time.trueor"fat"– Fat LTO. Maximum optimisation, but linking can become extremely slow."off"– Disable LTO completely, which can speed up linking for large projects that don't need it.
For a release binary where performance matters, lto = "thin" is a common recommendation. Fat LTO is usually reserved for final distribution builds where every last percent of performance counts.
codegen-units – parallelisation during code generation
A single crate can be split into multiple code generation units, allowing rustc to compile parts of it in parallel. More units → faster compilation, but potentially less effective optimisation because LLVM sees smaller chunks.
- In
dev, the default is256, which maximises parallelism. - In
release, the default is16, a compromise between speed and optimisation quality.
Setting codegen-units = 1 gives LLVM the largest possible view of your crate and can produce marginally better code, but makes compilation noticeably slower.
panic – stack unwinding or immediate abort
Controls what happens when your program panics at runtime.
'unwind'– The standard behaviour: the stack is unwound, destructors run, and a panic message is produced.'abort'– The process terminates immediately. No stack unwinding.
Choosing 'abort' reduces the size of the final binary because the unwinding machinery is not included. It can also prevent certain soundness issues in unsafe code where unwinding across FFI boundaries is problematic.
Aborting inside tests can break the test harness:
Tests, benchmarks, build scripts, and proc macros always use 'unwind' regardless of this setting because the test runner relies on unwinding to catch panics. Setting panic = 'abort' in your profile will not affect tests; they will still unwind.
strip – removing symbols or debuginfo
This setting controls the -C strip flag. Stripping reduces binary size by removing information that is not needed at runtime.
"none"– keep everything (default for both profiles)."debuginfo"– remove debug information but keep symbol names."symbols"– remove both debug info and symbol names (the smallest binary).
Stripping is off by default even in release, because symbols are useful for profiling and crash reports. When you are ready to ship a final binary to end users, adding strip = "symbols" can shrink the executable significantly.
debug-assertions and overflow-checks
debug-assertionsenables thedebug_assert!macro and activates thecfg(debug_assertions)conditional compilation flag. Indev, this istrue; inrelease,false. Debug assertions are runtime checks that are too expensive for production, like verifying invariants inside data structures.overflow-checkscontrols runtime integer overflow checks. Whentrue, arithmetic overflow panics. Whenfalse, overflow wraps silently in release mode (matching the behaviour of--releasein other compiled languages). The default inreleaseisfalsefor performance.
incremental – reusing previous compilation work
When enabled, rustc saves intermediate artefacts to disk so that only the parts of your code that changed need to be recompiled. This is a massive speedup for development, so it is true in dev and false in release.
Customising a profile
To change a setting, add a section for the profile you want to modify to the Cargo.toml at the root of your package.
Example: faster debug builds with basic optimisations
Compiling without any optimisation can make programs so slow that they are difficult to test. Setting opt-level = 1 gives a noticeable speed boost while still compiling quickly.
[profile.dev]
opt-level = 1
After this change, cargo build will use optimisation level 1 instead of 0.
Example: optimised release binaries with minimal size
This combination is typical for CLI tools distributed to users:
[profile.release]
opt-level = "z" # smallest binary
lto = true # fat LTO for maximum size reduction
codegen-units = 1 # give LLVM the whole crate at once
strip = "symbols" # remove everything not needed at runtime
panic = 'abort' # smaller binary, no unwinding tables
Verify the profile was applied:
After modifying your profile, run cargo build --release and check the output message. If the binary size or behaviour matches your expectations, the custom profile is working. You can also inspect the actual flags passed to rustc with cargo build --release -vv.
Common mistakes and misconceptions
Believing that dependency profile settings apply to your binary. Profile sections inside a dependency’s Cargo.toml are silently ignored. Only the top‑level package and Cargo configuration files matter.
Using opt-level = 3 for everything without measuring. Level 3 is not always the fastest; in some programs level 2 wins, and "s" can be faster than "z" depending on cache behaviour. Profile your program before committing to a setting.
Turning on lto = true without realising the link‑time cost. Fat LTO can turn a 10‑second link into a 2‑minute one. Start with lto = "thin" and only move to true if you can justify the time.
Setting codegen-units = 1 without understanding the compile‑time trade‑off. It can increase compilation time by 20‑30% for a few percent of runtime improvement. Use it only for final distribution builds.
Confusing debug-assertions with overflow-checks. They are separate settings. Disabling overflow-checks in a profile where debug-assertions are still on is valid, though unusual. Be explicit if you need one and not the other.
A practical workflow for tweaking profiles
When you start a project, use the defaults. As the project grows, ask two questions:
- Are my debug builds too slow to test? If yes, raise
opt-levelto1in thedevprofile. - Is my release binary too large or slow? If yes, first measure the current binary size and benchmark. Then experiment with
lto,codegen-units,strip, andpanicone change at a time. Re‑measure after each change.
Tuning profiles is an empirical process. The settings that work well for one crate may be wrong for another. The cargo subcommand cargo bloat (from the cargo-bloat crate) can help identify what is taking up space in your binary.
Profile‑specific rustflags are still unstable:
If you need to pass custom flags like -C target-cpu=native only for release builds, the stable approach is to set the RUSTFLAGS environment variable conditionally in your build script. Cargo’s built‑in profile-rustflags feature is still nightly‑only as of Rust 1.83. Check the tracking issue for progress.
Summary
Build profiles let you separate the concerns of fast development from the demands of production releases without juggling compiler flags by hand. The dev profile optimises for low compile time and rich diagnostics; the release profile optimises for runtime speed and binary size. Every setting has a clear default, and every default can be overridden in Cargo.toml when your needs diverge.
The most important takeaway: never ship a binary built with the dev profile. It will be slow, large, and contain debug assertions that should never run in production. Always use --release (and possibly a customised [profile.release]) for anything you distribute.