Build Profiles in Cargo.toml
Learn how to configure compiler settings for development and release builds using Cargo profiles, and optimize your Tauri app binary size and performance.
Every time you run cargo build or cargo tauri build, the Rust compiler makes a series of choices about how aggressively to optimise your code, whether to include debug information, and how much effort to spend on making the final binary small. Those choices are not random — they come from a build profile.
In a Tauri project, the Rust backend lives inside the src-tauri directory, and its configuration is the Cargo.toml file sitting in that folder. The [profile] section in that file is where you tell the compiler exactly what you want. These settings apply when you run tauri build as part of Building & Distribution.
What Are Build Profiles?
A build profile is a named collection of settings that control the Rust compiler's behaviour during compilation. It determines things like:
- how hard the compiler works to make your code run faster
- whether debug symbols are kept in the final executable
- how many parallel units the compiler uses to build your crate
- whether the binary gets stripped of unnecessary symbols
These settings matter because the right trade-offs look very different when you are iterating on your code in development versus when you are shipping the final application to users.
Think of it like photography presets:
A profile is like a camera preset. You have one preset for quick snapshots (fast compile, easy to debug) and another for a final print‑ready image (slow to process, optimised for quality and file size). You switch between them with a single word — dev or release.
The Built‑in Profiles: dev and release
Cargo comes with four built‑in profiles: dev, release, test, and bench. The two you will work with every day are dev and release. Cargo picks one automatically depending on the command you run.
cargo buildorcargo run→ dev profilecargo build --releaseorcargo tauri build→ release profile
The defaults are designed so you can start working without touching any configuration, but you can override any of them in Cargo.toml.
The dev Profile
The dev profile prioritises compilation speed and debuggability over runtime performance. It disables optimisations, includes full debug information, and turns on runtime checks that catch mistakes early.
The default dev settings look like this internally:
[profile.dev]
opt-level = 0
debug = true
debug-assertions = true
overflow-checks = true
lto = false
panic = "unwind"
incremental = true
codegen-units = 256
Compilation is fast, but the resulting binary will be noticeably larger and slower than a release build. That trade‑off is deliberate — you want the feedback loop to be as short as possible while you are writing code.
Don't ship dev binaries to users:
A dev binary can be up to 10× larger than a release binary and run an order of magnitude slower. It also contains full debug symbols that make reverse engineering trivial. Never distribute a dev build as your final application.
The release Profile
The release profile goes all‑in on runtime speed and binary size at the expense of compilation time. It turns on LLVM’s most aggressive optimisations, strips debug information, and disables runtime sanity checks.
[profile.release]
opt-level = 3
debug = false
strip = "none"
debug-assertions = false
overflow-checks = false
lto = false
panic = "unwind"
incremental = false
codegen-units = 16
When you run cargo tauri build, Tauri invokes Cargo with the release profile. That means every setting in [profile.release] in your src‑tauri/Cargo.toml directly affects the final executable your users download.
Custom Profiles
You can define your own profiles that inherit from an existing one and tweak only the settings you care about. This is useful when you need a specialised build — for example, a “production” build that keeps debug symbols for crash reporting, or a “size” build that trades even more speed for a smaller binary.
A custom profile is declared with inherits and any overrides:
# src-tauri/Cargo.toml
[profile.tauri-release]
inherits = "release"
strip = true
lto = true
codegen-units = 1
opt-level = "s"
You use it by passing the --profile flag:
cargo tauri build --profile tauri-release
The custom profile name becomes part of the output directory: target/tauri‑release/. That lets you keep differently configured builds side by side without cleaning the target directory.
Key Settings That Control Your Build
Each profile setting maps directly to a Rust compiler flag. Understanding what they do helps you make intentional choices rather than copying snippets blindly.
opt‑level
Controls how much effort the compiler puts into making your code run fast. The options are:
| Value | Meaning |
|---|---|
0 | No optimisations (fastest compile) |
1 | Basic optimisations |
2 | Standard optimisations |
3 | Aggressive optimisations |
"s" | Optimise for binary size |
"z" | Optimise for binary size, disable loop vectorisation |
Higher numbers generally produce faster code but also increase compilation time and sometimes binary size. The size‑oriented levels "s" and "z" are especially valuable for desktop apps where a smaller installer matters.
debug
Controls how much debug information is baked into the binary. true (or 2) includes everything you need for a debugger; false (or 0) produces no debug info at all. The intermediate options like 1 or "line‑tables‑only" give you readable backtraces without storing variable names or types.
For a release build that you may need to debug in the field, debug = 1 is a pragmatic middle ground — you get useful panic messages and backtraces at a modest size cost.
lto
Link‑Time Optimisation lets LLVM see your entire program at once during linking and apply cross‑crate optimisations. It can significantly improve runtime performance and reduce binary size, but it makes linking much slower.
false— No cross‑crate LTO (fastest link)"thin"— Thin LTO, a good compromise between speed and benefittrueor"fat"— Full LTO, slow but most effective
For a final Tauri release, enabling lto = true often shrinks the binary by 10–20% and gives a measurable speed‑up, at the cost of a longer build.
strip
Removes symbols and debug information from the final binary. The options are:
"none"— Keep everything"debuginfo"— Remove debug info but keep symbol names"symbols"— Remove both (smallest binary, but no readable backtraces)
Stripping symbols makes the binary smaller but also makes crash reports much harder to read. A good pattern for a Tauri app is to strip only debug info and keep symbols, or to build with symbols and then upload them to a symbol server.
codegen‑units
Determines how many independent chunks the compiler divides your crate into for parallel compilation. More units speed up compilation (especially for the dev profile) but reduce optimisation opportunities. For release builds, setting codegen‑units = 1 gives the compiler the best chance to inline and remove dead code, at the cost of a noticeably longer compile.
Other Important Settings
- panic —
"unwind"(default) cleans up the stack on panic;"abort"terminates immediately, reducing binary size slightly. - incremental — When
true, saves intermediate compilation state to disk so that recompiles are faster. Useful in dev, should befalsein release for best optimisations. - overflow‑checks — When
true, integer overflow causes a runtime panic. Disabling it in release avoids the performance cost but means overflow wraps silently. - debug‑assertions — Enables
debug_assert!macros. Turned off in release for performance. - rpath — Controls whether the binary embeds runtime library search paths. Rarely changed in Tauri projects.
Optimising for Binary Size
Tauri apps are desktop applications that users download. A smaller binary means a faster download, less disk usage, and a better first impression. The release profile alone already helps, but you can go further.
A reliable size‑optimisation recipe looks like this:
# src-tauri/Cargo.toml
[profile.release]
opt-level = "z" # minimise size aggressively
lto = true # enable full link‑time optimisation
codegen-units = 1 # allow the most inlining and dead‑code elimination
strip = true # remove all symbols and debuginfo
panic = "abort" # smaller panic machinery
Real‑world impact:
Applying these settings to a typical Tauri app often cuts the final binary size by half or more compared to the default release profile. The trade‑off is a slower build — but you only pay that cost when you ship.
If you need to keep readable backtraces for error reporting, replace strip = true with strip = "debuginfo" and keep debug = 1. That retains enough information for a crash report while still removing the bulk of debug metadata.
Overriding Profiles for Dependencies
Sometimes the top‑level crate (your Tauri backend) needs to stay debuggable, but you want the dozens of dependencies it pulls in to be fully optimised. Profile overrides let you apply different settings to specific packages.
# src-tauri/Cargo.toml
[profile.dev.package."*"]
opt-level = 2 # optimise all dependencies
[profile.dev.package.tauri]
opt-level = 0 # but leave the tauri crate itself unoptimised for debugging
The wildcard "*" matches every dependency. You can then carve out exceptions for specific crates by naming them. This pattern is especially useful when you want fast compiles for your own code but still need dependencies to perform well enough during development.
Generic code lives where it is instantiated:
Generic functions and types are monomorphised in the crate that uses them, not the crate that defines them. If your app uses a generic function from a dependency that is compiled with opt-level = 0, that function won't be optimised even if you set a higher optimisation level for the dependency itself. Profile overrides don't cross that boundary.
Build Overrides for Build Scripts
Build scripts (build.rs files) are compiled and run on the host machine during the build. They are not part of your final binary, so they use a separate set of profile settings. You can override those with the build-override key:
[profile.release.build-override]
opt-level = 0 # compile build scripts quickly, even in release mode
This is useful when you want a fast development loop even while performing a release build.
Putting It Together: A Tauri Example
Below is a realistic src-tauri/Cargo.toml profile configuration for a Tauri v2 app that uses React + Vite for the frontend. It defines a custom tauri‑release profile that produces a small, fast binary while keeping just enough debug info for panic backtraces.
# src-tauri/Cargo.toml
[package]
name = "my-tauri-app"
version = "0.1.0"
edition = "2021"
[dependencies]
tauri = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[profile.tauri-release]
inherits = "release"
opt-level = "z"
lto = true
codegen-units = 1
strip = "debuginfo"
debug = 1
panic = "abort"
Build your app with the custom profile:
cargo tauri build --profile tauri-release
The output binary will live at src‑tauri/target/tauri‑release/my‑tauri‑app (or my‑tauri‑app.exe on Windows). It will be noticeably smaller than a default release build, yet still produce readable backtraces if a panic occurs.
Common Mistakes and How to Avoid Them
- Using the default release profile without size tuning — The default
opt-level = 3prioritises speed over size. For an end‑user desktop app, size usually matters more. At minimum, setopt-level = "s"and enable LTO. - Stripping all symbols and then trying to debug a crash — A
strip = truebinary gives you almost nothing in a crash report. Keepdebug = 1and strip only"debuginfo"if you need field diagnostics. - Forgetting that custom profile names are case‑sensitive —
cargo tauri build --profile Tauri‑Releasewill silently fall back to the default release profile if the exact name doesn't match. - Setting
codegen-units = 1in the dev profile — This makes incremental compiles extremely slow. Keep the default high value for dev and only drop it for release or custom production profiles. - Assuming profile overrides will optimise generic dependencies — As noted earlier, generics are monomorphised where they are used. If your top crate is unoptimised, a generic function from an optimised dependency may still end up unoptimised. This is a subtle but important limitation.
Summary
Build profiles are the primary way you control the output of the Rust compiler. By understanding the handful of settings that matter most — opt-level, lto, strip, codegen-units, and debug — you can dramatically reduce your Tauri app's binary size without sacrificing the ability to debug production issues. The default profiles get you started, and custom profiles let you tune exactly for your distribution needs.
When you combine a size‑optimised Rust backend with an efficient web frontend, you get the small, fast desktop app that Tauri promises.