Performance Best Practices
Optimize the measured hot path only. Rust gives zero-cost abstractions, but discipline on allocations, profiling, and release settings still wins in production.
How to Use This List
- Apply before major perf work or after latency SEVs.
- Pair with Benchmarking & Profiling for tooling.
- Require benchmark or profile evidence in perf PR descriptions.
A - Measure
- Benchmark and profile release binaries. Never tune debug builds for production SLOs.
- Establish baseline metrics before changes. Criterion or APM p50/p99 documented in PR.
- Use representative workloads. Real query shapes, payload sizes, concurrency levels.
- Separate CPU vs I/O bottlenecks. Profiles empty when waiting on network/DB.
- Re-measure after each change. One knob at a time.
B - Hot Path Code
- Borrow before clone in inner loops.
&str,&[T],Cowat boundaries. - Preallocate
Vec/Stringwithwith_capacity. Known sizes from input metadata. - Prefer iterators until profile says loop. Zero-cost abstractions are real in release.
- Avoid
Box<dyn>and dynamic dispatch in hot paths. Use enums or generics. - Batch I/O and DB round trips. N+1 queries dominate many Rust APIs.
C - Async Services
- Never block Tokio worker threads.
spawn_blockingfor CPU/sync IO. - Limit concurrent tasks with semaphores. Protect DB and upstream APIs.
- Use
tracingspans on handler boundaries. Find slow awaits in production. - Connection pool sized from load tests. sqlx pool not unlimited.
- Timeout all external calls. Prevent pile-up under slowness.
D - Build and Deploy
- Release profile:
opt-level = 3, thin LTO,codegen-units = 1for servers. Tune with data. - Strip symbols in shipped artifacts; keep debug info separately if needed. Crash symbolication.
- Disable unused crate features workspace-wide. Smaller binaries and faster compiles.
- Pin
target-cputo deployment baseline. Notnativeunless uniform hardware. - Track binary size on CLI/WASM releases.
cargo bloatin release checklist.
E - Data and Libraries
- Use Polars lazy queries; avoid premature
collect(). Polars 0.46+ vectorizes when possible. - Stream large CSV/Parquet instead of loading all. Memory bounds latency tails.
- serde borrow from input buffer when parsing.
#[serde(borrow)]on&strfields. - Choose
HashMapvsBTreeMapdeliberately. Cache and ordering tradeoffs. - SIMD only after scalar baseline profiled. Complexity tax is real.
FAQs
When is premature optimization OK?
Known anti-patterns (clone per row in tight loop) without profile; still verify after fix.
Perf budget in PR template?
Yes: before/after numbers or "no hot path change" checkbox.
Who owns perf regressions?
Author of change unless platform provides profiling support.
Criterion in CI?
Optional on dedicated runner; store baselines for critical crates.
Polars vs hand loops?
Use Polars for analytics; drop to Rust loops only for custom ops Polars lacks.
How handle perf tech debt?
Ticket with profile attached; prioritize by SLO impact.
Memory vs CPU tradeoff?
Document choice; servers often trade MB for latency if RAM is cheap.
Load test before ship?
Yes for user-facing APIs; Rust speed does not fix architectural N+1.
Profile staging?
Mirror prod data volume anonymized; synthetic micro-bench insufficient alone.
When stop optimizing?
When SLO met with headroom; invest in observability instead of micro-opts.
Related
- Performance Basics - introduction
- 30 Performance Rules - quick rules
- Release Tuning - profile flags
- Avoiding Clones - ownership
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+.