Reducing Binary Size
Rust binaries can grow large from monomorphization and debug symbols. Strip symbols, tune opt-level, disable unused features, and use panic = abort for smaller deploy artifacts.
Recipe
[profile.release]
opt-level = "z"
lto = true
panic = "abort"
strip = "symbols"cargo bloat --release --cratesWhen to reach for this: CLI distribution, embedded targets, WASM bundles, and container image slimming.
Working Example
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = "symbols"
[features]
default = []cargo build --release
ls -lh target/release/myapp
cargo bloat --release -n 30What this demonstrates:
opt-level = "z"optimizes for size over speed- Feature-minimal default reduces compiled code
cargo bloatshows largest functions/cratesstripremoves symbol tables from shipped binary
Deep Dive
Size Levers
| Lever | Effect |
|---|---|
| Fewer features on deps | Less code linked |
panic = abort | No unwinding tables |
strip | Smaller on disk |
| Dynamic linking (platform) | Rare in Rust servers |
upx compression | Last resort, AV issues |
WASM Size
[profile.release]
opt-level = "s"Run wasm-opt -Oz after build for WASM targets.
Gotchas
- Size opt without perf check - latency regression in API. Fix: benchmark critical paths after size tuning.
- Keeping all default features - pulls tokio full when only
rtneeded. Fix:default-features = false. - Many generic instantiations - duplicate code. Fix: type erasure on cold paths only.
- Debug symbols in Docker - huge layers. Fix: strip in final stage or separate debug symbol package.
- abort panics in library - consumers cannot catch. Fix: apps choose
panic = abort; libs usuallyunwind.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
opt-level = "s" | Balance size/speed | Extreme size need |
split-debuginfo | Keep symbols off binary | Need single-file debug |
| Static musl binary | Portable CLI | glibc DNS nuances |
FAQs
How big is too big?
Context-dependent: CLI < few MB ideal; server less critical than container count of deps.
Does LTO help size?
Yes, merges and dead-strips unused functions across crates.
Why is tokio big?
Enable only needed features: rt-multi-thread, macros, not full unless required.
clap size?
Use clap 4 with derive and disable unused string features; consider argh for tiny CLIs.
serde in binary?
Necessary for JSON APIs; avoid deriving on every internal type if not serialized.
strip vs split debuginfo?
strip for minimal artifact; split for crash reporting with external symbols.
WASM binary size?
wasm-pack build --release + wasm-opt; avoid pulling full std when possible.
musl static size?
Larger than dynamic glibc sometimes; trade portability vs MB.
Count monomorphization?
cargo bloat shows instantiations; reduce generic types in hot crate surface.
Docker distroless?
Small runtime image complements stripped Rust binary; still minimize binary itself.
Related
- Release Tuning - perf vs size profiles
- Profiles & Build Settings
- Useful Cargo Subcommands - bloat
- Minimal Docker Images
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.