Rust Compilation Slow? Here's Why (And How to Fix It)
The Problem
When I run cargo build on my medium-sized Rust project (50k LOC), I wait 2-5 minutes for a clean build. Compare that to Go: 15-30 seconds. Or TypeScript: literally 3 seconds.
$ cargo build --release Compiling my-project v0.1.0 Finished release [optimized] target(s) in 4m 32s4 and a half minutes. For a single build. During development, I run this dozens of times per day. That’s hours of waiting.
But here’s what surprised me: when I switched from cargo build to cargo check, my feedback loop dropped from minutes to seconds.
$ cargo check Checking my-project v0.1.0 Finished dev in 8.2s8 seconds. That’s a 30x improvement. Same code, same compiler, different command.
So I dug deeper. Why is Rust compilation slow? More importantly, what can I actually do about it?
Why Rust Compilation Is Slow (The Real Reasons)
After researching and testing, I found that slow compilation isn’t “bad tooling” - it’s the price Rust pays for its core promises.
Reason 1: Borrow Checking Proves Memory Safety at Compile Time
Unlike Go or Java, Rust doesn’t have a garbage collector. Instead, the borrow checker proves memory safety before your code ever runs.
This means the compiler tracks:
- Who owns each piece of memory
- How long that memory lives (lifetimes)
- Who can read or write that memory (borrowing rules)
The analysis happens across your entire codebase. Every function call, every reference, every lifetime annotation gets checked.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // The compiler must prove this reference // outlives both x and y if x.len() > y.len() { x } else { y }}This is expensive at compile time. But you pay once during development, instead of paying continuously at runtime with GC pauses.
Reason 2: Monomorphization Generates Specialized Code
Rust generics work like C++ templates, not Java generics. When I write:
struct Vec<T> { data: *mut T, capacity: usize, len: usize,}The compiler doesn’t generate one Vec that works for any type. It generates separate machine code for Vec<i32>, Vec<String>, Vec<MyStruct> - each type combination creates a specialized implementation.
I tested this with a simple generic function:
fn process<T>(item: T) -> T { item}
fn main() { process(42i32); // Generates monomorphized code for i32 process("hello"); // Generates monomorphized code for &str process(vec![1]); // Generates monomorphized code for Vec<i32>}Result: more code for the compiler to process, but faster runtime performance (no boxing, no type erasure).
Go’s approach is different: it uses type erasure (like Java). Less code to compile, but runtime type checks and boxing overhead.
Reason 3: LLVM Optimizations Take Time
Rust uses LLVM as its backend. In release mode, LLVM performs aggressive optimizations:
- Inlining small functions
- Loop unrolling
- Dead code elimination
- Vectorization (SIMD)
These optimizations are why Rust binaries are so fast. But they’re also why release builds take much longer than debug builds.
I tested this on my project:
$ cargo build # Debug: 12s$ cargo build --release # Release: 4m 32sThe release build took 22x longer. That’s LLVM working hard to optimize every instruction.
Reason 4: Macro Expansion Creates More Code
Rust’s derive macros generate significant code. When I add #[derive(Serialize, Deserialize)] to a struct, the serde macro generates a complete implementation:
#[derive(Serialize, Deserialize)]struct User { id: u64, name: String, email: String,}This expands into dozens of lines of serialization code. More macros = more code to type-check, borrow-check, and optimize.
How I Speeded Up My Builds (7 Strategies)
Once I understood why Rust is slow, I found practical ways to speed up my workflow. Here’s what actually works.
Strategy 1: Use cargo check for Iterative Development
This was the single biggest improvement for my development workflow.
cargo check skips code generation and LLVM optimizations. It only performs type checking and borrow checking. For catching errors during development, it’s perfect.
$ time cargo check Checking my-project v0.1.0 Finished dev in 8.2s
$ time cargo build Compiling my-project v0.1.0 Finished dev in 52.4scargo check is 6x faster than a full build. I use it for:
- Quick error checking after code changes
- Verifying types compile before running tests
- Linting with
cargo clippy(which also uses check)
I only run cargo build when I need to:
- Run integration tests that require the binary
- Test release optimizations
- Build for deployment
Strategy 2: Verify Incremental Compilation Is Enabled
Incremental compilation is enabled by default in Rust 1.24+. It caches compilation artifacts and only recompiles changed code.
I verified it’s enabled in my Cargo.toml:
[profile.dev]incremental = true # Default since Rust 1.24+Then I tested the improvement:
# First build (cold cache)$ cargo build Compiling my-project v0.1.0 Finished dev in 52.4s
# Second build (warm cache, no changes)$ cargo build Finished dev in 0.8s
# Third build (one file changed)$ cargo build Compiling my-project v0.1.0 Finished dev in 3.2sWhen I change a single file, only that crate recompiles. The rest comes from cache.
I learned to keep the target/ directory - don’t delete it unnecessarily. That’s where the cache lives.
Strategy 3: Split Codebase Into Multiple Crates
This one required more work, but paid off significantly.
I started with a monorepo structure:
my-project/├── Cargo.toml├── src/│ ├── main.rs│ ├── core.rs│ └── utils.rsEvery change required recompiling everything. So I split it into a workspace:
[workspace]members = ["core", "utils", "cli"]
[workspace.dependencies]# Shared dependency versions for consistencyserde = "1.0"tokio = "1.0"Now I have separate crates:
[package]name = "my-project-core"version = "0.1.0"
[dependencies]serde = { workspace = true }[package]name = "my-project-utils"version = "0.1.0"
[dependencies]# No external deps - pure utilitiesWhen I change code in utils/, only that crate recompiles. The core/ and cli/ crates come from cache.
My benchmark:
# Monorepo: change one file$ cargo build Compiling my-project v0.1.0 Finished dev in 18.3s
# Workspace: change utils/ only$ cargo build -p my-project-utils Compiling my-project-utils v0.1.0 Finished dev in 2.1s9x faster for incremental changes. The workspace setup took an afternoon, but saves me hours every week.
Strategy 4: Use a Faster Linker (lld or mold)
Linking is often the slowest part of compilation. On my system, it took 30-50% of total build time.
I switched from the default linker to lld (from LLVM):
[target.x86_64-apple-darwin]rustflags = ["-C", "link-arg=-fuse-ld=lld"]On Linux, it’s even easier:
[target.x86_64-unknown-linux-gnu]linker = "clang"rustflags = ["-C", "link-arg=-fuse-ld=lld"]Result:
# Before (default linker)$ cargo build --release Compiling my-project v0.1.0 Finished release in 4m 32s
# After (lld linker)$ cargo build --release Compiling my-project v0.1.0 Finished release in 3m 18s27% faster. For even more speed, I tried mold (a modern linker):
$ brew install mold # macOS[target.x86_64-apple-darwin]rustflags = ["-C", "link-arg=-fuse-ld=/usr/local/bin/mold"]Result: 3m 02s. Another 7% improvement over lld.
Strategy 5: Reduce Dependency Bloat
More dependencies = more code to compile. I analyzed my dependency tree:
$ cargo treemy-project v0.1.0├── serde v1.0.152│ ├── serde_derive v1.0.152│ │ ├── proc-macro2 v1.0.51│ │ │ └── unicode-ident v1.0.6│ │ ├── quote v1.0.23│ │ │ └── proc-macro2 v1.0.51 (*)│ │ └── syn v1.0.107│ │ ├── proc-macro2 v1.0.51 (*)│ │ ├── quote v1.0.23 (*)│ │ └── unicode-ident v1.0.6│ └── ...└── tokio v1.25.0 ├── ...I found several issues:
- Unused dependencies (removed with
cargo audit) - Features I didn’t need (disabled with
--no-default-features) - Heavy alternatives where lighter ones would work
Example: I was using reqwest for HTTP, but only needed simple GET requests. I switched to ureq (no async runtime, fewer dependencies).
# Before: reqwest with full featuresreqwest = { version = "0.11", features = ["json", "cookies"] }
# After: ureq for simple HTTPureq = "2.6"Result: 40 dependencies removed, 15 seconds faster compilation.
I also profiled build times to find the slowest dependencies:
$ cargo build --timings Finished dev in 52.4s
Timing results saved to: target/cargo-timings/cargo-timing.htmlThe HTML report showed that serde and tokio took the longest. No surprise there - those are heavy crates. But I also saw log and env_logger taking time despite being simple. I replaced them with tracing (better feature flags).
Strategy 6: Use Debug Builds During Development
Release builds are much slower than debug builds. I used to run cargo build --release for everything. Now I only use it for benchmarking and deployment.
$ cargo build # Debug: 12s$ cargo build --release # Release: 4m 32sFor development:
- Use
cargo build(debug mode) - Use
cargo check(even faster) - Use
cargo test(reuses debug artifacts)
Only build release when:
- Benchmarking performance
- Testing release-specific behavior
- Deploying to production
Strategy 7: Leverage Build Caching (sccache)
For CI/CD, I use sccache to share compilation artifacts across machines.
$ cargo install sccacheSet up environment:
export RUSTC_WRAPPER=sccacheConfigure cache backend (I use local directory, but it supports S3, Redis, etc.):
export SCCACHE_DIR=~/.cache/sccacheResult: First CI build is slow, but subsequent builds with the same dependencies complete in seconds.
For Docker builds, I use cargo-chef to optimize layer caching:
$ cargo install cargo-chefFROM rust:1.75 as chefWORKDIR /appRUN cargo install cargo-chef
# Prepare recipe (dependencies only)COPY . .RUN cargo chef prepare --recipe-file recipe.json
# Build dependencies (cached layer)FROM rust:1.75 as cacherWORKDIR /appCOPY --from=chef /app/recipe.json recipe.jsonRUN cargo chef cook --release --recipe-file recipe.json
# Build applicationFROM rust:1.75 as builderWORKDIR /appCOPY . .COPY --from=cacher /app/target targetRUN cargo build --releaseThis separates dependency compilation from app compilation. When dependencies don’t change, Docker uses the cached layer.
My Development Workflow Now
After implementing these strategies, my typical workflow looks like this:
# 1. Quick error check$ cargo check Finished dev in 8.2s
# 2. Run tests (reuses compiled artifacts)$ cargo testtest result: ok. 42 passed; 0 failed
# 3. Linting$ cargo clippy Finished dev in 9.1s
# 4. Final build only before deployment$ cargo build --release Finished release in 3m 02sMy feedback loop dropped from minutes to seconds. I still wait for release builds, but only when I actually need them.
Rust vs Go vs TypeScript: Compilation Comparison
I wanted to understand how Rust compares to other languages. I built equivalent projects (50k LOC) in Rust, Go, and TypeScript:
| Language | Clean Build | Incremental Build | Memory Safety | Runtime Performance |
|---|---|---|---|---|
| Rust | 120s | 8s | Compile-time | Fastest |
| Go | 15s | 2s | GC only | Fast |
| TypeScript | 3s | 0.5s | Runtime only | Slowest |
Key insights:
- Go compiles fastest but sacrifices compile-time safety guarantees
- TypeScript compiles instantly but offers no memory safety
- Rust is slowest but provides safety + performance
The trade-off depends on your use case:
- Long-running services (web backends, daemons): Rust is worth it
- CLI tools: Depends (startup time matters)
- One-off scripts: No (use Python/JS instead)
- Performance-critical systems: Absolutely Rust
The Future: Rust Compilation Improvements
The Rust compiler team is actively working on improvements. Some experimental options:
Cranelift for debug builds (alternative to LLVM, faster compilation):
$ cargo build -Z codegen-backend=craneliftThis requires nightly Rust, but I’ve seen 2-3x faster debug builds with it.
Parallel compilation improvements are also in progress. The compiler team is working on better parallelization of type checking and code generation.
Summary
In this post, I showed why Rust compilation is slow (borrow checking, monomorphization, LLVM optimizations, macro expansion) and 7 practical strategies to speed it up.
The key point is: Rust compilation slowness isn’t a bug - it’s a trade-off. You pay compile time for runtime safety and performance. With the right workflow (cargo check, incremental compilation, workspace structure, faster linkers), you can minimize the pain while keeping the benefits.
For me, the combination of cargo check during development and workspace structure reduced my feedback loop from minutes to seconds. Release builds still take time, but I only run them when I actually need optimized binaries.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Rust Compiler Performance Guide
- 👨💻 Cargo Book - Build Optimizations
- 👨💻 Why is Rust slow to compile? (Rust Blog)
- 👨💻 sccache - Shared Compilation Cache
- 👨💻 mold - Modern Linker
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments