Sharing State in Rust Web Applications
James Reed
Infrastructure Engineer · Leapcell

Introduction
Building robust and scalable web services often involves managing shared resources across multiple threads or asynchronous tasks. In the world of Rust, known for its strict ownership rules and emphasis on thread safety, this challenge takes on a particularly interesting and deliberate form. While the Arc<Mutex<T>> pattern is a fundamental cornerstone for concurrent programming in Rust, web frameworks like actix-web introduce their own convenient abstractions. This article will explore the nuances of sharing state in a multi-threaded web application context, specifically comparing the direct use of Arc<Mutex<T>> with actix-web's web::Data<T> and illustrating their application with practical examples. Understanding these patterns is crucial for anyone building high-performance, thread-safe web services in Rust.
Core Concepts for Concurrent State
Before diving into the comparisons, let's briefly define the core components that underpin concurrent state management in Rust:
-
std::sync::Arc<T>(Atomic Reference Counted): This smart pointer provides shared ownership of a value. MultipleArcinstances can point to the same data, and the data is only dropped when the lastArcpointing to it is dropped. Crucially,Arcallows the shared data to be safely passed between threads. It's often paired with an interior mutability type. -
std::sync::Mutex<T>(Mutual Exclusion): A primitive for protecting shared data from concurrent access. To access the data inside aMutex, a thread must first acquire a lock. If the lock is already held by another thread, the current thread will block until the lock is released. This ensures that only one thread can modify the data at a time, preventing race conditions. -
actix_web::web::Data<T>: This isactix-web's convenient wrapper aroundArc<T>. It allows you to register shared application state with theactix-webapplication, making it automatically available to handlers as an extractor. Essentially,Data<T>isArc<T>with added ergonomic sugar tailored for web applications. -
Handlers and Middleware: In
actix-web, handlers are functions that respond to HTTP requests, and middleware functions can process requests and responses before or after handlers. Both often need access to shared application state.
Sharing Resources: Arc<Mutex<T>> vs. actix_web::web::Data<T>
At their core, actix_web::web::Data<T> and a manually managed Arc<Mutex<T>> serve the same fundamental purpose: providing shared, thread-safe access to mutable state within an actix-web application. The main difference lies in their integration and convenience.
The Direct Arc<Mutex<T>> Approach
When you manage Arc<Mutex<T>> directly, you explicitly wrap your shared data structure and pass clones of the Arc to wherever it's needed. This provides maximum flexibility but can be slightly more verbose, especially when setting up the server.
Consider a simple counter that different requests can increment or decrement:
use std::sync::{Arc, Mutex}; use actix_web::{web, App, HttpServer, Responder, HttpResponse}; // Our shared application state struct AppState { counter: Mutex<i32>, } async fn increment_counter(data: web::Data<Arc<AppState>>) -> impl Responder { let mut counter = data.counter.lock().unwrap(); *counter += 1; HttpResponse::Ok().body(format!("Counter: {}", *counter)) } async fn get_counter(data: web::Data<Arc<AppState>>) -> impl Responder { let counter = data.counter.lock().unwrap(); HttpResponse::Ok().body(format!("Counter: {}", *counter)) } #[actix_web::main] async fn main() -> std::io::Result<()> { let app_state = Arc::new(AppState { counter: Mutex::new(0), }); HttpServer::new(move || { App::new() .app_data(web::Data::new(Arc::clone(&app_state))) // Register the Arc<AppState> .route("/increment", web::post().to(increment_counter)) .route("/get", web::get().to(get_counter)) }) .bind(("127.0.0.1", 8080))? .run() .await }
In this example, AppState contains a Mutex<i32>. We create an Arc<AppState> and explicitly clone it when calling app_data. The handler then expects web::Data<Arc<AppState>> as an extractor. This works perfectly fine and shows the raw power of Arc<Mutex<T>>.
The actix_web::web::Data<T> Approach
actix_web::web::Data<T> simplifies the pattern by effectively wrapping your Arc<T> (or even Arc<Mutex<T>> directly) and providing it as a direct extractor. When you use web::Data::new(my_state), actix-web handles the Arc creation and cloning internally, making the setup cleaner.
Let's refactor the previous example using web::Data<T> more idiomatically:
use std::sync::Mutex; use actix_web::{web, App, HttpServer, Responder, HttpResponse}; // Shared application state - no need for explicit Arc here, Data handles it struct AppState { counter: Mutex<i32>, } async fn increment_counter_data(data: web::Data<AppState>) -> impl Responder { let mut counter = data.counter.lock().unwrap(); *counter += 1; HttpResponse::Ok().body(format!("Counter: {}", *counter)) } async fn get_counter_data(data: web::Data<AppState>) -> impl Responder { let counter = data.counter.lock().unwrap(); HttpResponse::Ok().body(format!("Counter: {}", *counter)) } #[actix_web::main] async fn main() -> std::io::Result<()> { // We can pass the state directly to web::Data, it will wrap it in an Arc let app_state = web::Data::new(AppState { counter: Mutex::new(0), }); HttpServer::new(move || { App::new() .app_data(app_state.clone()) // Clone the Data<AppState> .route("/increment", web::post().to(increment_counter_data)) .route("/get", web::get().to(get_counter_data)) }) .bind(("127.0.0.1", 8080))? .run() .await }
Notice the key differences:
- In
main,AppStateis wrapped directly byweb::Data::new(), which implicitly usesArcinternally. - The
app_data(app_state.clone())line now clones theweb::Datainstance itself, not a rawArc. - Handlers simply take
web::Data<AppState>, making access straightforward.
When to Choose Which
-
Use
web::Data<T>for general application state: This is the recommended and most ergonomic way to share configuration, database connection pools, or other global state that needs to be accessible to all handlers within anactix-webapplication. It abstracts away theArcboilerplate. -
Direct
Arc<Mutex<T>>within custom object hierarchies or complex scenarios: If you have internal components or services that are not directlyactix-webhandlers but still need to share state and adhere to Rust's concurrency model, then managingArc<Mutex<T>>explicitly within those structures gives you more control. For example, if you build a background worker thread that also modifies the same state, you'd pass it anArc<Mutex<T>>clone directly. Whileweb::Data<T>isArc<T>internally, it's primarily designed as an extractor foractix-webhandlers.
Application Scenarios
Both patterns are essential for common web service tasks:
- Database Connection Pools: An
Arc<PgPool>orArc<SqlitePool>is frequently wrapped inweb::Datato provide database access to all handlers. - Configuration Settings: Global application configuration can be loaded once and made available via
web::Data<AppConfig>. - Caching: An in-memory cache shared across requests would typically be
Arc<Mutex<HashMap<K, V>>>or similar, exposed throughweb::Data. - Rate Limiting: A global rate limiter state that tracks request counts per user/IP address would definitely involve
Arc<Mutex<T>>.
Conclusion
Sharing resources in a multi-threaded Rust web application requires careful consideration of thread safety and ownership. The Arc<Mutex<T>> pattern provides the foundational elements for this, ensuring safe, concurrent access to mutable data. actix-web's web::Data<T> builds upon this foundation, offering an ergonomic and idiomatic way to inject application-specific state into handlers. While both ultimately achieve similar results by leveraging Arc, web::Data<T> simplifies the developer experience for common web application state management, making it the preferred choice for seamlessly integrating shared resources into your actix-web services.

