Skip to main content
Lesson 8 of 12Beginner⏱ 10 mins

Sharing State

Application state.

58% of path

Learning Objectives

  • Inject shared data into handlers

##Sharing data between handlers

Handlers are independent functions, but real applications need shared things: a database pool, configuration, a cache, a counter. Actix Web shares them with application state.

#Register state with app_data

Wrap the value in web::Data and register it on the App. Handlers receive it by adding a web::Data<T> argument:

code_blocksrust
use actix_web::{get, web, App, HttpServer, Responder};

struct AppState {
    app_name: String,
}

#[get("/")]
async fn index(data: web::Data<AppState>) -> impl Responder {
    format!("Hello from {}", data.app_name)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let state = web::Data::new(AppState { app_name: "ActixWeb".into() });

    HttpServer::new(move || {
        App::new()
            .app_data(state.clone())
            .service(index)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

web::Data is an Arc under the hood, so state.clone() is cheap and every worker points to the same value.

#Mutable state needs a lock

Handlers run on many threads at the same time, so Rust will not let you mutate shared data without synchronization. Wrap the field in a Mutex:

code_blocksrust
use std::sync::Mutex;

struct Counter {
    visits: Mutex<u32>,
}

#[get("/visit")]
async fn visit(data: web::Data<Counter>) -> impl Responder {
    let mut visits = data.visits.lock().unwrap();
    *visits += 1;
    format!("Visits: {}", *visits)
}

Create the web::Data<Counter> outside the HttpServer::new closure and clone it inside. If you create it inside the closure, every worker would get its own separate counter.

#Keep locks short

Never hold a std::sync::Mutex guard across an .await. Lock, change the value, and release it before awaiting anything. For read-heavy data use RwLock, and for counters prefer atomics such as AtomicUsize.

#What belongs in state

Good candidates are database pools (they are already thread safe, so no Mutex), configuration, HTTP clients and caches. Avoid storing per-request data there; use extractors for that.

Interactive Code Inspector

Select any highlighted line of code below to deeply inspect its mechanics, understand why it exists, and what happens if you change it.

src/main.rs
1use actix_web::{get, web, App, HttpServer, Responder};
2
3struct AppState {
4 app_name: String,
5}
6
7#[get("/")]
8async fn index(data: web::Data<AppState>) -> impl Responder {
9 format!("Hello from {}!", data.app_name)
10}
11
12#[actix_web::main]
13async fn main() -> std::io::Result<()> {
14 let state = web::Data::new(AppState {
15 app_name: "Actix Web Tutorial".to_string(),
16 });
17
18 HttpServer::new(move || {
19 App::new()
20 .app_data(state.clone())
21 .service(index)
22 })
23 .bind(("127.0.0.1", 8080))?
24 .run()
25 .await
26}
Line 14
web::Data::new(...)

What it does

Wraps the state in an `Arc` (Atomic Reference Counted pointer).

Why use it?

State must be safely shared across multiple threads.

How it works underneath

Creates a thread-safe shared reference.

If you remove it

State could not be shared across worker threads safely.

"What If?" Experiment

Learning by doing. Modify the code to see what happens.

Goal

Prove why state must be created outside the closure.

Starting Code
HttpServer::new(move || App::new().app_data(state.clone()).service(count))
Suggested Modification

Move the web::Data::new(...) call inside the closure and reload the page several times with .workers(4).

Expected Result

The counter appears to jump back and forth because each worker has its own copy.

Explanation: Requests are spread across workers. With one Data per worker, each one counts separately; one shared Data gives a single consistent count.

Common Mistakes

Anticipate and avoid these frequent pitfalls when working with this concept.

!

Creating state inside the factory closure

The closure runs once per worker, so each worker gets a separate counter.

Incorrect Pattern
HttpServer::new(|| { App::new().app_data(web::Data::new(Counter { visits: Mutex::new(0) })) })
Correct Pattern
let state = web::Data::new(Counter { visits: Mutex::new(0) }); HttpServer::new(move || App::new().app_data(state.clone()))

Explanation: Build the Data once, move it into the closure and clone it there so every worker shares the same value.

!

Holding a Mutex guard across an await

The guard stays locked while the task is suspended, which can block other requests or deadlock.

Incorrect Pattern
let mut n = data.visits.lock().unwrap(); some_async_call().await; *n += 1;
Correct Pattern
{ let mut n = data.visits.lock().unwrap(); *n += 1; } some_async_call().await;

Explanation: Limit the lock to a small scope and release it before awaiting. Use tokio::sync::Mutex only if you truly must hold it across an await.

Practical Exercise

Build a visit counter

Task

Create a GET /count endpoint that increments and returns a shared counter.

Requirements

  • Store the counter in state using a Mutex
  • Create the state outside the HttpServer closure
  • Return the new value on every request

Need a hint?

Try solving the exercise in your local environment first. Once you have a solution, or if you get completely stuck, reveal the answer.

Real-World Application

Production services share a database connection pool, loaded configuration, a metrics registry and HTTP clients through web::Data. The pool is already thread safe, so it needs no Mutex.

Key Takeaways

  • web::Data<T> is an Arc, so cloning it is cheap and all workers share one value.
  • Register with .app_data(state.clone()) and read with a web::Data<T> handler argument.
  • Mutating shared state needs Mutex, RwLock or atomics.
  • Create shared state outside the HttpServer::new closure, otherwise each worker gets its own copy.

Finished reading?

Mark this lesson as done to track your progress and earn XP.