Debug and Release Builds
Cargo uses build profiles to control compiler optimizations, debug symbol generation, and runtime safety checks. The two primary profiles are Debug (dev, the default for cargo build and cargo run) and Release (release, enabled with --release or -r).
dev (Debug) vs release Profile Comparison
| Setting | dev Profile (cargo build) |
release Profile (cargo build --release) |
|---|---|---|
| Output directory | target/debug/ |
target/release/ |
Optimization level (opt-level) |
0 (no optimizations) |
3 (maximum speed optimizations) |
| Compilation speed | Fast (skips expensive LLVM passes) | Slower (runs full inlining, loop vectorization, dead-code elimination) |
| Runtime execution speed | Slow (often 10× to 100× slower than release on tight loops!) | Maximum native speed |
Debug info (debug) |
full (2 — full line numbers and variable tables for lldb/gdb) |
none (0 — omitted by default) |
Integer overflow checks (overflow-checks) |
true (panics on +, -, * overflow) |
false (wraps around using two's complement) |
debug_assert! macros |
Enabled (checked at runtime) | Disabled (completely stripped at compile time) |
Incremental compilation (incremental) |
true |
false (optimizes the whole crate from scratch) |
Rule of Thumb: Use Debug builds during everyday coding and debugging for fast compile turnarounds. Always use Release builds (
--release) when benchmarking performance, profiling CPU usage, or deploying binaries to production.
Conditional Behavior: debug_assert! and cfg!(debug_assertions)
Rust provides built-in macros that execute only when debug assertions are active (the default in dev and test profiles) and compile down to nothing in --release builds:
| Construct | Behavior in Debug (dev) |
Behavior in Release (--release) |
|---|---|---|
assert!(cond) |
Checked; panics if false |
Checked; panics if false (always active!) |
debug_assert!(cond) |
Checked; panics if false |
Completely stripped (zero runtime cost) |
debug_assert_eq!(a, b) |
Checked; panics if a != b |
Completely stripped (zero runtime cost) |
cfg!(debug_assertions) |
Evaluates to true at compile time |
Evaluates to false at compile time |
fn expensive_invariant_check(slice: &[i32]) -> bool {
println!(" [Running O(n) sorted invariant check...]");
slice.windows(2).all(|w| w[0] <= w[1])
}
fn binary_search_first(sorted: &[i32]) -> Option<i32> {
// Runs in debug builds to catch bugs early; costs 0 ns in `--release` builds!
debug_assert!(expensive_invariant_check(sorted), "slice must be sorted");
if cfg!(debug_assertions) {
println!("Mode: Debug build (debug_assertions = true)");
} else {
println!("Mode: Release build (debug_assertions = false)");
}
sorted.first().copied()
}
fn main() {
let data = [10, 20, 30, 40];
println!("First element: {:?}", binary_search_first(&data));
}
Output under cargo run (Debug):
[Running O(n) sorted invariant check...]
Mode: Debug build (debug_assertions = true)
First element: Some(10)
Output under cargo run --release (Release):
Mode: Release build (debug_assertions = false)
First element: Some(10)
Customizing Profiles in Cargo.toml
You can override any setting in [profile.dev] or [profile.release] inside Cargo.toml to tune compilation time, runtime speed, or binary size:
[package]
name = "my_app"
version = "0.1.0"
edition = "2024"
# Speed up external dependencies in debug builds while keeping your own code fast to compile:
[profile.dev.package."*"]
opt-level = 2
# Customize production release builds for maximum performance and smaller binary size:
[profile.release]
opt-level = 3 # 0, 1, 2, 3, "s" (size), or "z" (min size)
lto = "thin" # Link-Time Optimization across crates ("thin" or "fat")
codegen-units = 1 # Maximizes LLVM optimization across the crate
strip = "symbols" # Strips symbol tables to shrink executable file size
overflow-checks = false # Set to `true` if you want overflow panics in production too
panic = "unwind" # Or "abort" to remove stack-unwinding tables
The [profile.dev.package."*"] Trick
If your project uses CPU-heavy dependencies (such as image decoding, cryptography, compression, or game physics), running them at opt-level = 0 in debug mode can feel sluggish. Adding [profile.dev.package."*"] opt-level = 2 compiles third-party dependencies once with optimizations while keeping your own crate at opt-level = 0 for instant incremental rebuilds.