Actix Web Data vs. State Extractors A Dual Approach to Application State
Daniel Hayes
Full-Stack Engineer · Leapcell

Introduction
Building robust and scalable web applications in Rust often involves thoughtfully managing application state. This state can range from database connections and configuration settings to user sessions and caching layers. In the Actix Web framework, two primary mechanisms stand out for injecting and accessing such state within your handlers: Data and State extractors. While both serve the purpose of state management, they are designed for distinct use cases and offer different advantages. Understanding their intricacies is crucial for writing efficient, maintainable, and idiomatic Actix Web applications. This article will delve into the specifics of Data and State, contrasting their principles, implementations, and ideal application scenarios.
Core Concepts
Before diving into the extractors themselves, let's establish a clear understanding of some fundamental concepts frequently encountered in Actix Web state management.
- Application State: Refers to data or resources that need to be shared across multiple requests or available globally throughout the web application's lifecycle. Examples include database pools, Redis clients, configuration structures, or custom service instances.
 - Request-Scoped State: Data or resources that are relevant only for a single incoming HTTP request and its processing. This could include user authentication information parsed from headers, request-specific tracing IDs, or temporary data generated during request handling.
 - Extractor: In Actix Web, an extractor is a mechanism that allows you to easily retrieve specific data from the incoming HTTP request. This data can be anything from URI parameters and query strings to JSON request bodies and, as we'll see, application state. Extractors simplify handler signatures and enforce type safety.
 - Arc (Atomic Reference Counted): A smart pointer in Rust that allows multiple owners of a value. The value is dropped when the last 
Arcmanaging it is dropped.Arcis crucial for sharing data immutably across multiple threads, which is common in asynchronous web servers. - Mutex (Mutual Exclusion): A synchronization primitive used to protect shared data from concurrent access by multiple threads. It ensures that only one thread can access the protected data at any given time, preventing data corruption. 
Arc<Mutex<T>>is a common pattern for sharing mutable state across threads. 
Data Extractor: Application-Wide Shared State
The Data extractor in Actix Web is designed for injecting application-wide, immutable or safely mutable shared state into your handlers. When you register state with App::app_data, Actix Web wraps it in an Arc. This Arc is then available to all services (routes) within that application instance.
Principle
The core principle behind Data is to provide a way for your application to share essential, long-lived resources efficiently. By wrapping the state in Arc, Actix Web ensures that the state can be accessed safely from multiple worker threads without compromising thread safety. If the state needs to be mutable, you'll typically combine Arc with an internal mutability primitive like Mutex or RwLock.
Implementation
Let's illustrate with an example where we want to share a database connection pool throughout our application.
use actix_web::{web, App, HttpResponse, HttpServer}; use sqlx::{PgPool, postgres::PgPoolOptions}; use std::sync::Arc; // Define a simple struct to hold our configuration #[derive(Clone)] struct AppConfig { connection_string: String, // Other config fields } async fn index(pool: web::Data<PgPool>, config: web::Data<AppConfig>) -> HttpResponse { // Access the database pool let _result = sqlx::query!("SELECT 1").fetch_one(pool.as_ref()).await; // Access configuration println!("DB connection string: {}", config.connection_string); HttpResponse::Ok().body("Hello from Actix Web!") } #[actix_web::main] async fn main() -> std::io::Result<()> { let database_url = "postgres://user:password@localhost/database"; let pool = PgPoolOptions::new() .max_connections(5) .connect(&database_url) .await .expect("Failed to create PgPool."); let app_config = AppConfig { connection_string: database_url.to_string(), }; HttpServer::new(move || { App::new() .app_data(web::Data::new(pool.clone())) // Register the PgPool .app_data(web::Data::new(app_config.clone())) // Register the AppConfig .route("/", web::get().to(index)) }) .bind(("127.0.0.1", 8080))? .run() .await }
In this example:
- We initialize a 
PgPoolandAppConfigonce. - We use 
App::app_data(web::Data::new(my_data))to register these resources with the application. Notice that for larger types likePgPool,.clone()is necessary becauseapp_dataexpects the state to beClone(as it's often moved into different worker threads).web::Dataeffectively wraps it in anArc. - In the 
indexhandler, we can simply addweb::Data<PgPool>andweb::Data<AppConfig>as arguments, and Actix Web will automatically extract and inject the correct instance. 
Use Cases
- Database connection pools (
PgPool,SqlitePool,RedisPool) - Configuration settings (
AppConfig,EnvConfig) - Shared service clients (e.g., S3 client, payment gateway client)
 - Global caches (e.g., 
Arc<RwLock<HashMap<K, V>>>) 
State Extractor: Request-Scoped Contextual State
The State extractor (previously web::ReqData in older Actix Web versions, now typically referred to more generically or through custom extractors) is designed for injecting data that is specific to the current request and might be added or modified during the request lifecycle by middleware or earlier request processors. Unlike Data, which is configured once for the entire application, State allows you to attach data dynamically per-request.
Principle
The principle of State is to enrich the context of a request as it flows through the application pipeline. Middleware or custom pre-processing steps can insert information relevant only to that specific request, which can then be conveniently accessed by subsequent handlers or other middleware. This data is typically dropped once the request processing completes.
Implementation
While Actix Web's web::Data is explicit about application-wide state, request-scoped state is often managed using the HttpRequest object's extensions or by defining custom extractors that operate on the HttpRequest. A common pattern for adding request-scoped state involves middleware.
Let's imagine we have an authentication middleware that parses a user ID from a token and wants to make this UserId available to downstream handlers.
use actix_web::{dev::{ServiceRequest, ServiceResponse, Transform}, web, App, Error, HttpRequest, HttpResponse, HttpServer}; use futures::future::{ok, Ready, LocalBoxFuture}; use std::{rc::Rc, future::{ready, Future}}; // A simple UserId struct to store in request extensions #[derive(Debug, Clone)] struct UserId(u32); // Our custom middleware pub struct AuthMiddleware; impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware where S: actix_web::Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, S::Future: 'static, B: 'static, { type Response = ServiceResponse<B>; type Error = Error; type InitError = (); type Transform = AuthMiddlewareService<S>; type Future = Ready<Result<Self::Transform, Self::InitError>>; fn new_transform(&self, service: S) -> Self::Future { ok(AuthMiddlewareService { service: Rc::new(service) }) } } pub struct AuthMiddlewareService<S> { service: Rc<S>, } impl<S, B> actix_web::Service<ServiceRequest> for AuthMiddlewareService<S> where S: actix_web::Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, S::Future: 'static, B: 'static, { type Response = ServiceResponse<B>; type Error = Error; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; fn poll_ready(&self, cx: &mut core::task::Context<'_>) -> core::task::Poll<Result<(), Self::Error>> { self.service.poll_ready(cx) } fn call(&self, req: ServiceRequest) -> Self::Future { let svc = self.service.clone(); Box::pin(async move { // Simulate authentication and user ID extraction // In a real app, this would involve parsing headers, checking tokens, etc. let user_id = UserId(123); // For demo, hardcoding user ID // Store the UserId in the request extensions // This is where request-scoped state is added req.extensions_mut().insert(user_id.clone()); let res = svc.call(req).await?; Ok(res) }) } } // Handler that extracts the UserId using web::ReqData (implicitly, or a custom extractor) async fn user_profile(user_id: web::ReqData<UserId>) -> HttpResponse { println!("Accessed by User ID: {:?}", user_id.0); HttpResponse::Ok().body(format!("User Profile for ID: {}", user_id.0)) } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .wrap(AuthMiddleware) // Apply our authentication middleware .route("/profile", web::get().to(user_profile)) }) .bind(("127.0.0.1", 8080))? .run() .await }
In this example:
AuthMiddlewareis introduced to simulate authentication.- Inside the middleware's 
callmethod, after hypothetical authentication, aUserIdinstance is created. - Crucially, 
req.extensions_mut().insert(user_id.clone());is used to store thisUserIdwithin the request's local extensions. This makes theUserIdavailable for the lifetime of this specific request. - The 
user_profilehandler then usesweb::ReqData<UserId>as an argument. Actix Web automatically looks for aUserIdtype in the request's extensions and extracts it. 
Use Cases
- Authenticated user details (
UserId,SessionToken) - Parsed request-specific parameters from custom headers
 - Request tracing IDs (
X-Request-ID) - Temporary data generated by a middleware to pass to downstream handlers
 
Data vs. State: A Comparison
| Feature | web::Data<T> (Application-wide State) | web::ReqData<T> (Request-scoped State/Extensions) | 
|---|---|---|
| Scope | Entire application instance | Single HTTP request | 
| Lifetime | Until application shuts down | Until request processing completes | 
| Creation | Configured once during App::new() setup | Dynamically added by middleware/extractors per request | 
| Mutability | Immutable by default; Arc<Mutex<T>> for safe mutation | Mutable within the request scope (e.g., HttpRequest::extensions_mut()) | 
| Underlying Type | Arc<T> (often) | Stored in HttpRequest::extensions (internally Box<dyn Any>) | 
| Access Pattern | Injected via App::app_data | Injected via req.extensions_mut().insert() in middleware or through custom extractors | 
| Typical Use | Database pools, env config, shared clients | Authenticated user, request ID, parsed tokens | 
Conclusion
Both web::Data and the concept of request-scoped State (often accessed via web::ReqData) are indispensable for managing information within Actix Web applications. web::Data is your go-to for resources that are consistent and shared across all requests and threads, providing an efficient way to distribute application-wide context. Conversely, request-scoped State is ideal for dynamic information that is generated on a per-request basis—perfect for enriching the request context with details such as authentication or tracing data. By thoughtfully choosing between these two mechanisms, developers can create well-structured, performant, and maintainable Actix Web services that effectively separate global concerns from request-specific contexts.

