Features & Optional Dependencies
Cargo features are the standard way to compile optional code paths, wire optional dependencies, and let consumers pay only for what they enable.
Recipe
[features]
default = ["std"]
std = ["dep:log"]
metrics = ["dep:prometheus"]
[dependencies]
log = { version = "0.4", optional = true }
prometheus = { version = "0.13", optional = true }#[cfg(feature = "metrics")]
pub fn export_metrics() { /* ... */ }When to reach for this: Optional backends, platform-specific code, or trimming compile time for library consumers.
Working Example
A library with database backends selected by feature:
[features]
default = ["postgres"]
postgres = ["dep:sqlx", "sqlx/postgres"]
sqlite = ["dep:sqlx", "sqlx/sqlite"]
[dependencies]
sqlx = { version = "0.8", optional = true, default-features = false }#[cfg(feature = "postgres")]
pub async fn connect(url: &str) -> sqlx::Result<sqlx::PgPool> {
sqlx::PgPool::connect(url).await
}
#[cfg(feature = "sqlite")]
pub async fn connect(url: &str) -> sqlx::Result<sqlx::SqlitePool> {
sqlx::SqlitePool::connect(url).await
}cargo build --no-default-features --features sqlite
cargo test --all-featuresWhat this demonstrates:
- Optional dependencies are inactive until a feature enables them
- Feature names should describe capability, not implementation detail
dep:sqlxsyntax ties a feature directly to an optional crate- CI should test
--no-default-featuresand--all-features
Deep Dive
Feature Composition
[features]
full = ["json", "metrics"]
json = ["dep:serde", "serde/derive"]- Features can enable other features (union semantics)
- Enabling
fullpulls in everything it lists - Avoid cycles: feature A must not enable B if B enables A
cfg in Code
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
compile_error!("enable at most one database backend");- Combine
feature,target_os, and customcfgflags - Use
compile_error!for mutually exclusive feature sets - Document required feature combinations in crate docs
Rust Notes
#[cfg(feature = "std")]
use std::collections::HashMap;
#[cfg(not(feature = "std"))]
use hashbrown::HashMap;#[cfg(test)]is separate from Cargo featurescfg_attrattaches attributes conditionallyrequired-featureson[[example]]gates runnable demos
Gotchas
- Implicit default features on deps - you pull in heavy code unintentionally. Fix:
default-features = falseand explicit feature list. - Feature unification across workspace - one member enabling a feature enables it for all crates sharing that dependency. Fix: split incompatible backends into separate crates if needed.
- Breaking feature renames - downstream
features = ["old"]breaks silently at compile time. Fix: treat feature renames as semver-major for libraries. - No features for internal-only modules - using
pub+ feature for every private helper creates API confusion. Fix: keep internal modules private; gate only public surface. - Testing only default features - optional paths rot. Fix: matrix CI with
--all-featuresand per-feature jobs.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Separate crates | Backends are large and mutually exclusive | Small optional 50-line module |
target_cfg only | OS-specific code without optional deps | User-selectable functionality |
cfg-if crate | Many nested cfg branches | Simple one-flag gates |
FAQs
Can features be enabled at runtime?
No. Features are compile-time only. Runtime toggles use config files or environment variables after building the right feature set.
How do dependents enable my features?
my_crate = { version = "1", features = ["metrics"] } in their Cargo.toml.
What is weak dependency syntax?
dep:serde in a feature table explicitly enables optional dependency serde without enabling serde's own default features unless you also list them.
Should default features be minimal?
For libraries, yes when compile time matters. For apps, defaults can include everything you ship.
How do I test a single feature?
cargo test --no-default-features --features sqlite builds only that configuration.
Can binaries and libraries share features?
Workspace [workspace.metadata] or shared feature names in [workspace.dependencies] keep naming consistent across members.
What about feature unification bugs?
If two crates need incompatible versions of the same optional dep, split into separate workspace packages or use different package names via renaming (advanced).
How do I document features?
List each feature in README and rustdoc with compile command examples. crates.io shows feature flags automatically from the manifest.
Are feature names stable API?
Treat them like public API for libraries. Renaming or removing features is a breaking change for consumers who enabled them.
How does this interact with build.rs?
build.rs can emit cargo:rustc-cfg=feature="foo" for detected platform capabilities, complementing manifest features.
Related
- Cargo.toml Explained - manifest tables
- Profiles & Build Settings - size vs speed
- Build Scripts (build.rs) - custom cfg flags
- Configuration & Feature Design - app-level design
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+.