When using 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.

Extension Basics with Clone

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.

Extension with Arc for Shared State

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.

Cloning Cost Comparison

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.

Memory Footprint

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.

Mutation Semantics

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.

Interior Mutability Patterns

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.

Clone Requirements

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.

Nested Arc Considerations

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.

Performance Benchmark Concept

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).

Thread Safety Guarantees

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.

Handler Parameter Extraction

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.

Multiple Extensions

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.

When to Use Each Approach

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.

Summary Table

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)

Synthesis

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.