Loading page…
Rust walkthroughs
Loading page…
axum::Extension, what are the implications of cloning vs sharing state via Arc?axum::Extension stores state in http::Extensions as type-erased values, retrieving them via Clone when handlers request them. When you add a type that implements Clone, Extension clones it for each handler that needs it. Wrapping state in Arc<T> changes this dynamic: Arc::clone() is cheap (just incrementing a reference count), so each handler shares the same underlying data rather than receiving independent copies. The choice between cloning and Arc affects memory usage, mutation semantics, and performance: cloning creates independent copies per request, while Arc shares a single instance across all concurrent requests.
use axum::{
Extension,
Router,
routing::get,
};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
config: String,
counter: usize,
}
async fn handler(Extension(state): Extension<AppState>) -> String {
// state is cloned from the stored Extension
format!("Config: {}, Counter: {}", state.config, state.counter)
}
#[tokio::main]
async fn main() {
let state = AppState {
config: "production".to_string(),
counter: 0,
};
let app = Router::new()
.route("/", get(handler))
.layer(Extension(state));
// Each handler invocation clones AppState
}Extension requires Clone and creates a new copy for each handler.
use axum::{
Extension,
Router,
routing::get,
};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
config: String,
counter: usize,
}
async fn handler(Extension(state): Extension<Arc<AppState>>) -> String {
// state is Arc::clone() - cheap reference count increment
// All handlers share the same underlying AppState
format!("Config: {}, Counter: {}", state.config, state.counter)
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState {
config: "production".to_string(),
counter: 0,
});
let app = Router::new()
.route("/", get(handler))
.layer(Extension(state));
// Each handler invocation does Arc::clone() - same instance shared
}Arc makes cloning cheap while sharing the underlying data.
use axum::Extension;
use std::sync::Arc;
#[derive(Clone)]
struct SmallState {
value: u64, // 8 bytes, cheap to clone
}
#[derive(Clone)]
struct LargeState {
data: Vec<u8>, // Could be megabytes
cache: Vec<String>, // Could be thousands of entries
mappings: Vec<(String, String)>, // Could be large
}
impl LargeState {
fn clone_size(&self) -> usize {
std::mem::size_of::<Self>() +
self.data.capacity() +
self.cache.iter().map(|s| s.capacity()).sum::<usize>()
}
}
async fn small_handler(Extension(state): Extension<SmallState>) -> String {
// Cloning SmallState is trivial
format!("Value: {}", state.value)
}
async fn large_handler(Extension(state): Extension<LargeState>) -> String {
// Cloning LargeState could be expensive
format!("Data len: {}", state.data.len())
}
// Better: use Arc for large state
async fn large_handler_arc(Extension(state): Extension<Arc<LargeState>>) -> String {
// Arc::clone() is cheap regardless of data size
format!("Data len: {}", state.data.len())
}Clone cost scales with data size; Arc::clone() is constant-cost.
use axum::Extension;
use std::sync::Arc;
#[derive(Clone)]
struct Config {
settings: Vec<String>, // Let's say 10KB of data
}
// With cloning: each concurrent request has its own copy
// 100 concurrent requests = 100 * 10KB = 1MB of Config data
// With Arc: all requests share one copy
// 100 concurrent requests = 1 * 10KB = 10KB of Config data
// Plus 100 * sizeof(Arc<Config>) ≈ 100 * 8 bytes for the pointers
async fn config_clone(Extension(config): Extension<Config>) -> String {
config.settings.join(", ")
}
async fn config_arc(Extension(config): Extension<Arc<Config>>) -> String {
config.settings.join(", ")
}
fn demonstrate_memory() {
let config = Config {
settings: (0..1000).map(|i| format!("setting_{}", i)).collect(),
};
// Clone approach: memory scales with concurrency
let configs: Vec<Config> = (0..100).map(|_| config.clone()).collect();
println!("Clone memory: {} copies", configs.len());
// Arc approach: memory is constant
let arc_config = Arc::new(config);
let config_refs: Vec<Arc<Config>> = (0..100).map(|_| Arc::clone(&arc_config)).collect();
println!("Arc memory: {} references to 1 copy", config_refs.len());
}Arc keeps memory constant regardless of request concurrency.
use axum::Extension;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
// Cloning: mutations don't affect other copies
#[derive(Clone)]
struct MutableState {
counter: usize, // Each clone has independent counter
}
async fn mutate_clone(Extension(mut state): Extension<MutableState>) -> String {
state.counter += 1; // Only affects this handler's copy
format!("Counter: {}", state.counter) // Other handlers unaffected
}
// Arc with interior mutability: mutations affect all references
#[derive(Clone)]
struct SharedState {
counter: Arc<AtomicUsize>, // Shared counter
}
async fn mutate_arc(Extension(state): Extension<Arc<SharedState>>) -> String {
let prev = state.counter.fetch_add(1, Ordering::SeqCst);
format!("Counter: {}", prev + 1) // Visible to all handlers
}
// Arc without interior mutability: can't mutate
#[derive(Clone)]
struct ImmutableState {
config: String, // Shared read-only data
}
async fn read_only(Extension(state): Extension<Arc<ImmutableState>>) -> String {
// Can read, cannot mutate
state.config.clone()
}Cloning creates independent copies; Arc shares mutations if interior mutability is used.
use axum::Extension;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::RwLock;
// Pattern 1: Atomic for simple counters
#[derive(Clone)]
struct AtomicState {
requests: Arc<AtomicUsize>,
}
async fn atomic_handler(Extension(state): Extension<Arc<AtomicState>>) -> String {
let count = state.requests.fetch_add(1, Ordering::SeqCst);
format!("Request #{}", count + 1)
}
// Pattern 2: RwLock for complex mutable data
#[derive(Clone)]
struct RwLockState {
data: Arc<RwLock<Vec<String>>>,
}
async fn rwlock_handler(
Extension(state): Extension<Arc<RwLockState>>,
) -> String {
// Multiple readers can proceed concurrently
let data = state.data.read().unwrap();
format!("Items: {}", data.len())
}
async fn rwlock_write_handler(
Extension(state): Extension<Arc<RwLockState>>,
) -> String {
// Writers get exclusive access
let mut data = state.data.write().unwrap();
data.push(format!("item_{}", data.len()));
format!("Added item, total: {}", data.len())
}
// Pattern 3: Mutex for exclusive access
use std::sync::Mutex;
#[derive(Clone)]
struct MutexState {
data: Arc<Mutex<Vec<String>>>,
}
async fn mutex_handler(
Extension(state): Extension<Arc<MutexState>>,
) -> String {
let mut data = state.data.lock().unwrap();
data.push("new item".to_string());
format!("Total: {}", data.len())
}Arc with interior mutability enables shared state modifications.
use axum::Extension;
use std::sync::Arc;
// Extension requires Clone
// This works: small, cheap to clone
#[derive(Clone)]
struct CheapState {
id: u64,
}
async fn cheap_handler(Extension(state): Extension<CheapState>) -> String {
format!("ID: {}", state.id)
}
// This works: Arc<T> implements Clone
async fn arc_handler(Extension(state): Extension<Arc<CheapState>>) -> String {
format!("ID: {}", state.id)
}
// This DOESN'T work: not Clone
// struct NotCloneState {
// data: Vec<u8>,
// }
// async fn not_clone_handler(Extension(state): Extension<NotCloneState>) -> String {
// "won't compile".to_string()
// }
// This works: Arc makes anything Clone
async fn arc_not_clone_handler(
Extension(state): Extension<Arc<Vec<u8>>>
) -> String {
format!("Data len: {}", state.len())
}Arc makes any type work with Extension by implementing Clone.
use axum::Extension;
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
db: Arc<Database>, // Already Arc
config: Arc<Config>, // Already Arc
}
#[derive(Clone)]
struct Database {
pool: String,
}
#[derive(Clone)]
struct Config {
settings: String,
}
async fn nested_arc(Extension(state): Extension<AppState>) -> String {
// state is cloned (Arc<Database> and Arc<Config> are cloned cheaply)
format!("DB: {}, Config: {}", state.db.pool, state.config.settings)
}
async fn arc_of_state(Extension(state): Extension<Arc<AppState>>) -> String {
// Arc<AppState> cloned, which clones Arc<Database> and Arc<Config>
// Same result, extra Arc layer
format!("DB: {}, Config: {}", state.db.pool, state.config.settings)
}
// Both work, but Arc<Arc<T>> is redundant
// Prefer AppState with Arc fields over Arc<AppState>If fields are already Arc, wrapping the whole struct in Arc is redundant.
use axum::Extension;
use std::sync::Arc;
use std::time::Instant;
#[derive(Clone)]
struct BigState {
data: [u8; 1024], // 1KB per clone
}
#[derive(Clone)]
struct BigStateArc {
data: Arc<[u8; 1024]>, // ~8 bytes per clone (pointer)
}
fn benchmark_cloning() {
let big = BigState { data: [0u8; 1024] };
let big_arc = BigStateArc { data: Arc::new([0u8; 1024]) };
// Clone BigState 1000 times
let start = Instant::now();
let clones: Vec<BigState> = (0..1000).map(|_| big.clone()).collect();
let clone_time = start.elapsed();
println!("BigState clone x1000: {:?}", clone_time);
// Clone BigStateArc 1000 times
let start = Instant::now();
let arc_clones: Vec<BigStateArc> = (0..1000).map(|_| big_arc.clone()).collect();
let arc_time = start.elapsed();
println!("BigStateArc clone x1000: {:?}", arc_time);
// Arc cloning is ~1000x faster for this case
// Because it copies 8 bytes instead of 1024 bytes
}Arc::clone() is O(1); struct cloning is O(size of struct).
use axum::Extension;
use std::sync::Arc;
// Extension requires Clone, not Send/Sync
// But handlers run on different threads, so:
// If state is Clone + Send, each handler gets its own copy
#[derive(Clone)]
struct SendState {
data: String,
} // String is Send + Clone, so SendState is Send + Clone
// If state is Arc<T>, T must be Send + Sync for sharing across threads
#[derive(Clone)]
struct SharedState {
data: Arc<String>, // String is Send + Sync, so Arc<String> is Send + Sync
}
// Arc<Mutex<T>> requires T: Send (for access from any thread)
use std::sync::Mutex;
struct MutexState {
data: Arc<Mutex<Vec<String>>>, // Vec<String> only needs Send
}
// Arc<RwLock<T>> requires T: Send + Sync (for shared references)
use std::sync::RwLock;
struct RwLockState {
data: Arc<RwLock<Vec<String>>>, // Vec<String> needs Send + Sync
}Arc sharing requires Send + Sync for the inner type.
use axum::Extension;
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
db: String,
config: String,
}
// Extract Extension directly
async fn handler1(Extension(state): Extension<AppState>) -> String {
state.config.clone()
}
// Extract Arc directly
async fn handler2(Extension(state): Extension<Arc<AppState>>) -> String {
state.config.clone()
}
// Can't extract both in same handler
// async fn handler3(
// Extension(state): Extension<AppState>,
// Extension(arc_state): Extension<Arc<AppState>>,
// ) -> String {
// // This won't work - can only have one Extension per type
// state.config.clone()
// }
// If you need both, use different types or extract Arc and clone manually
async fn handler3(Extension(state): Extension<Arc<AppState>>) -> String {
// If you really need an owned copy:
let owned = AppState::clone(&state);
format!("Owned: {}, Arc: {}", owned.config, state.config)
}One Extension per type; extract either value or Arc, not both.
use axum::Extension;
use std::sync::Arc;
#[derive(Clone)]
struct Database {
url: String,
}
#[derive(Clone)]
struct Config {
debug: bool,
}
async fn multi_handler(
Extension(db): Extension<Arc<Database>>,
Extension(config): Extension<Arc<Config>>,
) -> String {
format!("DB: {}, Debug: {}", db.url, config.debug)
}
#[tokio::main]
async fn main() {
let db = Arc::new(Database { url: "postgres://...".to_string() });
let config = Arc::new(Config { debug: true });
let app = axum::Router::new()
.route("/", axum::routing::get(multi_handler))
.layer(Extension(db))
.layer(Extension(config));
// Each Extension is stored separately
// Handlers extract what they need
}Multiple Extension layers for different state types.
use axum::Extension;
use std::sync::Arc;
// Use Clone (no Arc) when:
// 1. State is small and cheap to clone
// 2. You want independent copies per request
// 3. State is immutable or handler-local mutation is fine
#[derive(Clone)]
struct SmallConfig {
max_connections: u32, // 4 bytes
timeout_ms: u32, // 4 bytes
}
// Cloning SmallConfig is trivial - just copy 8 bytes
async fn small_handler(Extension(config): Extension<SmallConfig>) -> String {
format!("Max: {}, Timeout: {}", config.max_connections, config.timeout_ms)
}
// Use Arc when:
// 1. State is large (expensive to clone)
// 2. State needs to be shared across requests
// 3. You use interior mutability (Mutex, RwLock, Atomic)
struct LargeCache {
entries: Vec<(String, Vec<u8>)>, // Potentially megabytes
}
async fn large_handler(
Extension(cache): Extension<Arc<RwLock<LargeCache>>>
) -> String {
let cache = cache.read().unwrap();
format!("Entries: {}", cache.entries.len())
}
use std::sync::RwLock;Choose based on size, sharing needs, and mutation patterns.
| Aspect | Direct Clone | Arc-wrapped |
|--------|-------------|-------------|
| Clone cost | O(data size) | O(1) |
| Memory per request | Full copy | Pointer (8 bytes) |
| Mutations | Isolated per request | Shared (with interior mutability) |
| Thread safety required | None (copies are independent) | Send + Sync |
| Suitable for small state | Yes | Also works, slight overhead |
| Suitable for large state | No (expensive) | Yes |
| Suitable for shared counters | No (copies diverge) | Yes (with Atomic/Mutex) |
The choice between cloning and Arc centers on memory and mutation semantics:
Cloning creates isolated copies: Each handler invocation receives its own copy of the state. Mutations in one handler don't affect other handlers or subsequent requests. This is ideal for configuration that doesn't change, or for per-request state that should be independent. The cost is memory duplication: 100 concurrent requests with 1MB of state each means 100MB of memory used.
Arc shares a single instance: All handlers receive references to the same data. Arc::clone() costs only a reference count increment regardless of data size. This is essential for large state, shared caches, or counters. However, mutation requires interior mutability (Mutex, RwLock, or atomics), and mutations are visible to all handlers.
Key insight: Extension always clones when extracting. The question is what cloning means for your type. For Arc<T>, cloning means incrementing a reference count—a constant-time operation. For T directly, cloning means copying all fields. The performance difference becomes critical when state is large or request concurrency is high. Use Arc for large or shared state; use direct cloning for small, independent state.