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.

CommandDefault profile
cargo builddev
cargo rundev
cargo testtest (inherits from dev)
cargo build --releaserelease
cargo installrelease

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.

Settingdev defaultrelease defaultWhat it does
opt-level03Optimisation level for generated code
debugtrue (full debuginfo)falseAmount of debug information
debug-assertionstruefalseWhether debug_assert! and overflow checks are active
overflow-checkstruefalseRuntime checks for integer overflow
ltofalsefalseLink‑time optimisation
codegen-units25616How many parallel compilation units
panic'unwind''unwind'What happens on panic (unwind or abort)
incrementaltruefalseWhether to reuse previous compilation data
strip"none""none"Whether to remove symbols or debuginfo from the binary
rpathfalsefalseWhether 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 for release.
  • "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" or 0 – no debug info at all.
  • "line-tables-only" – file names and line numbers for backtraces.
  • "limited" or 1 – module‑level information without per‑variable details.
  • "full" or 2 – 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.
  • true or "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 is 256, which maximises parallelism.
  • In release, the default is 16, 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-assertions enables the debug_assert! macro and activates the cfg(debug_assertions) conditional compilation flag. In dev, this is true; in release, false. Debug assertions are runtime checks that are too expensive for production, like verifying invariants inside data structures.
  • overflow-checks controls runtime integer overflow checks. When true, arithmetic overflow panics. When false, overflow wraps silently in release mode (matching the behaviour of --release in other compiled languages). The default in release is false for 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.

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

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

  1. Are my debug builds too slow to test? If yes, raise opt-level to 1 in the dev profile.
  2. 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, and panic one 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.