The same versions of shared dependencies, avoiding the diamond dependency problem.
Features are optional dependencies or conditional compilation flags:
full = [ " serde " , " tokio " , " tracing " ]
serde = [ " dep:serde " , " dep:serde_json " ]
tracing = [ " dep:tracing " , " dep:tracing-subscriber " ]
serde = { version = " 1 " , optional = true }
serde_json = { version = " 1 " , optional = true }
tokio = { version = " 1 " , optional = true , features = [ " full " ] }
tracing = { version = " 0.1 " , optional = true }
tracing-subscriber = { version = " 0.3 " , optional = true }
#[cfg(feature = "serde" )]
use serde :: { Serialize , Deserialize };
#[cfg(feature = "serde" )]
#[derive( Serialize , Deserialize )]
#[cfg(not(feature = "serde" ))]
When two crates in the same dependency graph enable different features of a shared dependency, Cargo Unifies them — all enabled features are active for all dependents. This can cause unexpected Behavior:
shared = { version = " 1 " , features = [ " feature-x " ] }
shared = { version = " 1 " , features = [ " feature-y " ] }
In a workspace depending on both crate-a and crate-b``shared will have both feature-x and feature-y enabled. If feature-y has heavy dependencies, crate-a users pay the cost even though They only requested feature-x.
Keep features additive. Never use features to remove functionality. Use default = [] for library crates. Let users opt in to features. Use dep:serde syntax to gate the dependency itself, not just code. Document all features in the crate”s README. Profiles control compiler optimization settings:
opt-level = 0 # no optimization
debug = true # full debug info
debug-assertions = true # enable debug assertions
overflow-checks = true # integer overflow panics
lto = false # no link-time optimization
codegen-units = 256 # fast compile, no cross-crate optimization
incremental = true # incremental compilation
opt-level = 3 # maximum optimization
debug = false # no debug info
overflow-checks = false # wrapping arithmetic (no panics)
lto = true # link-time optimization
codegen-units = 1 # single codegen unit (better optimization, slower compile)
strip = true # strip symbols from binary
panic = " abort " # smaller binary (no unwinding)
cargo build --profile profiling
LTO performs optimizations across crate boundaries. It increases compile time significantly but can Reduce binary size by 10-20% and improve runtime performance by 5-10%.
lto = false: No LTO (default for dev)lto = "thin": Thin LTO — faster than full LTO, most of the benefitlto = true (or lto = "fat"): Full LTO — best optimization, slowest compileFor release builds in production, lto = "thin" is a good default. Use lto = true for maximum Performance-critical builds.
Cargo uses Semantic Versioning. A version requirement like "1.2" is equivalent to ">= 1.2.0, < 2.0.0":
serde = " 1 " # >= 1.0.0, < 2.0.0
tokio = " 1.35 " # >= 1.35.0, < 2.0.0
clap = " ^4.4 " # same as "4.4" (caret is default)
exact = " =1.0.0 " # exactly 1.0.0
range = " >=1.0, <2.0 " # explicit range
serde = { version = " 1 " , features = [ " derive " ] }
my-lib = { git = " https://github.com/user/my-lib " , branch = " main " }
local-lib = { path = " ../local-lib " }
tokio = { version = " 1 " , default-features = false , features = [ " rt-multi-thread " , " macros " ] }
# Optional dependency (only compiled when the feature is enabled)
optional-dep = { version = " 1 " , optional = true }
Cargo.lock pins exact dependency versions. It should be committed to version control for:
Binary crates (applications, CLIs) Libraries where exact reproducibility matters It should NOT be committed for library crates that are published to crates.io (the library’s Dependents should resolve their own compatible versions).
cargo update # update all dependencies within SemVer bounds
cargo update -p serde # update only serde
cargo update --precise 1.0.0 serde # pin serde to exact version
cargo build # debug build
cargo build --release # release build (optimized)
cargo build --target x86_64-unknown-linux-musl # cross-compile
cargo build -j 8 # parallel jobs (default: num_cpus)
cargo check # type-check without producing binary (fast)
cargo clean # remove target/
cargo test # run all tests
cargo test --release # run tests with release optimizations
cargo test -- --test-threads=1 # run tests sequentially (for debugging)
cargo test -- --nocapture # show println! output
cargo test my_module # run tests in specific module
cargo test -- test_name # run specific test by name
cargo doc # generate documentation
cargo doc --open # generate and open in browser
cargo doc --no-deps # only document this crate (not dependencies)
cargo doc --document-private-items # include private items
cargo clippy # run clippy lints
cargo clippy -- -W clippy::all -D warnings # treat warnings as errors
cargo fmt -- --check # check formatting without modifying files
cargo audit # check for known security vulnerabilities
cargo outdated # check for outdated dependencies
cargo tree # display dependency tree
cargo tree --duplicates # show duplicate dependencies
cargo udeps # find unused dependencies
cargo machete # determine which dependencies are unused
cargo login # authenticate with crates.io
cargo publish # publish to crates.io
cargo publish --dry-run # verify without publishing
cargo publish --allow-dirty # publish with uncommitted changes (not recommended)
Unit tests live in the same file as the code they test, inside a #[cfg(test)] module:
fn add (a : i32 , b : i32 ) -> i32 {
assert_eq! ( add ( 2 , 3 ), 5 );
assert_eq! ( add ( - 1 , 1 ), 0 );
#[should_panic(expected = "overflow" )]
// cargo test -- --ignored
Integration tests live in the tests/ directory and test the crate as an external consumer would:
│ │ └── mod.rs # shared test utilities
│ ├── integration_test.rs
fn test_add_from_external () {
assert_eq! ( add ( 10 , 20 ), 30 );
Each file in tests/ is compiled as a separate crate, so they cannot access src/ internals (only The public API). The tests/common/mod.rs pattern allows sharing test utilities.
Documentation tests are Rust code blocks in doc comments:
/// Adds two numbers together.
/// assert_eq!(add(2, 3), 5);
pub fn add (a : i32 , b : i32 ) -> i32 {
Doc tests are run by cargo test and serve as both documentation and tests. They verify that Examples in documentation actually compile and produce correct results.
use proptest :: prelude ::* ;
fn add_is_commutative (a in - 1000 i32 .. 1000 , b in - 1000 i32 .. 1000 ) {
assert_eq! ( my_crate :: add (a, b), my_crate :: add (b, a));
fn add_associative (a in - 1000 i32 .. 1000 , b in - 1000 i32 .. 1000 , c in - 1000 i32 .. 1000 ) {
let left = my_crate :: add ( my_crate :: add (a, b), c);
let right = my_crate :: add (a, my_crate :: add (b, c));
fn vec_sort_is_sorted (input in proptest :: collection :: vec ( proptest :: arbitrary :: any :: < i32 >(), 0 .. 100 )) {
let mut sorted = input . clone ();
for window in sorted . windows ( 2 ) {
prop_assert! (window[ 0 ] <= window[ 1 ]);
Proptest generates random inputs, finds minimal failing cases (shrinking), and can run thousands of Test cases per second. It is particularly effective for finding edge cases that hand-written tests Miss.
criterion = { version = " 0.5 " , features = [ " html_reports " ] }
use criterion :: {black_box, criterion_group, criterion_main, Criterion };
fn bench_add (c : &mut Criterion ) {
c . bench_function ( "add" , | b | {
b . iter ( || black_box ( my_crate :: add ( black_box ( 2 ), black_box ( 3 ))))
criterion_group! (benches, bench_add);
criterion_main! (benches);
Criterion provides statistical analysis (mean, median, standard deviation), regression detection, And HTML reports with plots. It is the standard benchmarking tool in the Rust ecosystem.
/// A 2D point in Euclidean space.
/// This struct represents a point with x and y coordinates.
/// let p = Point::new(1.0, 2.0);
/// assert_eq!(p.x(), 1.0);
/// This struct does not panic on construction.
/// This struct does not return errors.
/// This struct is safe to use from any thread.
/// * `x` - The x coordinate
/// * `y` - The y coordinate
pub fn new (x : f64 , y : f64 ) -> Self {
/// Returns the x coordinate.
/// This item is documented.
#[doc(alias = "Point2D" )]
#[doc(html_root_url = "https://docs.rs/my-crate/" )]
pub struct Point { /* ... */ }
/// This module contains internal utilities.
#[doc(hidden)] // hidden from documentation
pub mod internal { /* ... */ }
perf record -g target/release/my_binary
# Record with call graphs
perf record --call-graph dwarf target/release/my_binary
flamegraph = " 0.6 " # install with: cargo install flamegraph
cargo flamegraph --bin my-binary
# Generates flamegraph.svg
For async applications, tokio-console provides real-time task inspection:
tokio = { version = " 1 " , features = [ " tracing " ] }
console-subscriber = " 0.4 "
RUSTFLAGS = " --cfg tokio_unstable " cargo run
tracing-subscriber = { version = " 0.3 " , features = [ " env-filter " , " json " ] }
use tracing :: {info, warn, error, instrument};
use tracing_subscriber :: EnvFilter ;
tracing_subscriber :: fmt ()
EnvFilter :: try_from_default_env ()
. unwrap_or_else ( | _ | EnvFilter :: new ( "info" ))
. json () // structured JSON logging
info! ( "application started" );
async fn process_request (id : u64 ) {
info! ( "processing request" );
// Automatically logs function entry/exit with timing
The #[instrument] attribute automatically creates a span that logs function entry, exit, and Elapsed time. It captures all function arguments by default (use skip and fields to control what Is captured).
serde = { version = " 1 " , features = [ " derive " ] }
use serde :: { Serialize , Deserialize };
#[derive( Serialize , Deserialize , Debug )]
#[serde(default = "default_workers" )]
fn default_workers () -> usize { 4 }
let json = serde_json :: to_string ( & config) . unwrap ();
let parsed : Config = serde_json :: from_str ( & json) . unwrap ();
Serde is the de facto serialization framework. It supports JSON, YAML, TOML, MessagePack, CBOR, BSON, XML, and custom formats. The #[serde] attribute provides fine-grained control over field Names, defaults, serialization behavior, and conditional compilation.
tokio = { version = " 1 " , features = [ " rt-multi-thread " , " macros " , " net " , " io-util " , " fs " , " time " , " sync " ] }
Key features to enable:
rt-multi-thread: Multi-threaded schedulermacros: #[tokio::main] and #[tokio::test]net: TCP/UDP networkingio-util: Async I/O utilitiesfs: Async file system operationstime: Timers and delayssync: Async mutex, channels, watch, notifyclap = { version = " 4 " , features = [ " derive " ] }
#[command(name = "my-tool" )]
#[command(about = "A useful tool" , long_about = None )]
#[arg(short, long, default_value_t = false)]
/// Number of threads (default: number of CPUs)
#[arg(short = 'j' , long, default_value_t = num_cpus :: get())]
let args = Args :: parse ();
println! ( "input: {}, output: {:?}" , args . input, args . output);
let sum : i64 = ( 1 ..= 1_000_000 ) . par_iter () . sum ();
// Parallel map + collect
let results : Vec < i32 > = data . par_iter ()
. map ( | x | expensive_transform ( * x))
let mut data = vec! [ 3 , 1 , 4 , 1 , 5 , 9 , 2 , 6 ];
Rayon converts sequential iterators to parallel iterators by changing .iter() to .par_iter(). The work-stealing scheduler automatically balances load across threads.
use itertools :: Itertools ;
let data = vec! [ 1 , 2 , 3 , 4 , 5 ];
for chunk in & data . into_iter () . chunks ( 2 ) {
let chunk : Vec <_> = chunk . collect ();
println! ( "{:?}" , chunk); // [1, 2], [3, 4], [5]
for combo in ( 1 ..= 4 ) . combinations ( 2 ) {
let joined = vec! [ "a" , "b" , "c" ] . into_iter () . intersperse ( ", " ) . collect :: < String >();
let groups = vec! [ 1 , 1 , 2 , 3 , 3 , 3 ] . into_iter () . group_by ( |& k | k);
tracing-subscriber = " 0.3 "
use tracing :: {info, warn, error, span, Level };
use tracing_subscriber :: fmt;
let span = span! ( Level :: INFO , "request" , id = 42 );
let _guard = span . enter ();
info! ( "processing started" );
warn! ( "rate limit approaching" );
error! ( "database connection failed" );
tokio = { version = " 1 " , features = [ " full " ] }
serde = { version = " 1 " , features = [ " derive " ] }
use axum :: { Router , routing :: get, Json };
async fn hello () -> Json < Hello > {
Json ( Hello { message : "world" . into () })
let app = Router :: new () . route ( "/" , get (hello));
let listener = tokio :: net :: TcpListener :: bind ( "0.0.0.0:3000" ) .await. unwrap ();
axum :: serve (listener, app) .await. unwrap ();
Before adding a dependency, evaluate it:
Criterion How to Check Downloads crates.io page — monthly downloads Last update crates.io — last publish date Maintenance GitHub — open issues, PRs, commit frequency Dependencies cargo tree -p crate-name — dependency countBinary size impact cargo bloat --release — size contributionBuild time cargo build --timings — incremental and clean build timesLicense compatibility crates.io — license field MSRV README or Cargo.toml rust-version field Audit cargo audit — known CVEs