---
title: How to Use the Actor Model in Rust (actix, xtra)
summary: Use actix or xtra crates to define actor structs and spawn them in a runtime to handle messages asynchronously.
lang: en
slug: how-to-use-the-actor-model-in-rust-actix-xtra
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-use-the-actor-model-in-rust-actix-xtra/
---

# How to Use the Actor Model in Rust (actix, xtra)



## When shared state becomes a bottleneck

You are building a multiplayer game server. Player A jumps. Player B shoots. Player C chats. If you store the entire game world in one big struct protected by a `Mutex`, every action waits for the previous one to finish. The server chokes under contention. You need a way to let each player update their own slice of the world in parallel, without stepping on each other's toes.

That is where the actor model comes in. Instead of sharing memory, you split the world into independent units called actors. Each actor owns its state and communicates only by sending messages. This eliminates race conditions by design and scales naturally across threads.

## The actor model in plain words

An actor is a lightweight process that encapsulates state and behavior. It has three rules:

1. Actors keep their state private. No other code can read or write it directly.
2. Actors communicate by sending messages to each other.
3. Actors process messages one at a time, sequentially.

Think of actors like independent workers in a factory. Each worker has their own desk and tools. If Worker A needs something from Worker B, A does not walk over and grab it. A writes a note and drops it in B's mailbox. B reads the note when they have time and reacts.

This pattern removes the need for locks. Since only one actor touches its own data at a time, there is no concurrent access to protect. The compiler can verify this isolation, and the runtime handles the threading.

## Actors fit Rust's ownership perfectly

Rust's ownership model aligns with the actor model in a way that other languages struggle to match. When you spawn an actor, you move the initial state into it. The compiler guarantees that no other code holds a reference to that data. The actor boundary becomes an ownership boundary.

In languages without ownership, you can accidentally share a reference between actors and break isolation. In Rust, the borrow checker forces you to respect the boundary. If you try to hold a reference to actor state from outside, the compiler rejects you with E0382 (use of moved value). You must send a message.

This makes Rust actors safer than actors in dynamic languages. The compiler enforces the isolation rules at compile time, not just by convention.

## Minimal example with actix

The `actix` crate is the most popular actor framework for Rust. It provides a runtime, mailbox management, and lifecycle hooks. Here is a minimal actor that counts messages.

```rust
use actix::prelude::*;

/// An actor that counts how many messages it has received.
struct Counter {
    count: usize,
}

/// The Actor trait defines the lifecycle and context type.
impl Actor for Counter {
    /// Context<Self> provides a simple mailbox and address management.
    type Context = Context<Self>;
}

/// Message type to increment the counter.
struct Increment;

/// Handler defines how the actor reacts to a specific message.
impl Handler<Increment> for Counter {
    /// The Result type is the reply sent back to the sender.
    type Result = usize;

    /// handle runs when a message arrives.
    /// &mut self guarantees exclusive access to actor state.
    fn handle(&mut self, _msg: Increment, _ctx: &mut Self::Context) -> Self::Result {
        self.count += 1;
        self.count
    }
}

fn main() {
    // Create the actix system runtime.
    let sys = System::new();

    // Spawn the actor. start() moves the data into the actor.
    let addr = Counter { count: 0 }.start();

    // Send a message. do_send is fire-and-forget.
    addr.do_send(Increment);

    // Run the system until all actors stop.
    sys.run().unwrap();
}
```

The `Counter` struct holds the state. The `Actor` implementation tells the runtime how to manage the actor. The `Handler` implementation defines the reaction to `Increment` messages. When you call `addr.do_send(Increment)`, the message goes into the actor's mailbox. The actor loop pops the message and calls `handle`.

Notice the `&mut self` in `handle`. This is the key to safety. The actor runtime guarantees that `handle` runs sequentially. You get mutable access without locks. The compiler sees `&mut self` and knows no other code can touch the state while this handler runs.

Convention aside: use `do_send` for commands where you do not need a reply. Use `send` when you need to wait for a result. `send` returns a future that resolves to the `Result` type. Mixing these up is a common source of confusion. Stick to `do_send` for simple actions.

Trust the sequential guarantee. If `handle` has `&mut self`, you do not need `Mutex` inside the actor.

## Realistic example: Async actors

Real applications often need to do asynchronous work inside actors, like database queries or network requests. For that, you use `AsyncContext` instead of `Context`. `AsyncContext` integrates with `tokio` and lets you spawn async tasks that run inside the actor's mailbox loop.

```rust
use actix::prelude::*;
use std::time::Duration;

/// Message to trigger a delayed greeting.
struct DelayedGreet {
    name: String,
}

/// Actor that handles delayed tasks.
struct Greeter;

impl Actor for Greeter {
    /// AsyncContext enables async operations and timers.
    type Context = AsyncContext<Self>;
}

/// Handler for the initial request.
impl Handler<DelayedGreet> for Greeter {
    type Result = ();

    fn handle(&mut self, msg: DelayedGreet, ctx: &mut Self::Context) {
        // Schedule a task to run after 1 second.
        // The task will be processed by the same actor loop.
        ctx.notify_after(GreetTask(msg.name), Duration::from_secs(1));
    }
}

/// Internal task that runs after the delay.
struct GreetTask(String);

impl Handler<GreetTask> for Greeter {
    type Result = ();

    fn handle(&mut self, msg: GreetTask, _ctx: &mut Self::Context) {
        println!("Hello, {}!", msg.0);
    }
}

fn main() {
    let sys = System::new();
    let greeter = Greeter.start();
    greeter.do_send(DelayedGreet { name: "Rustacean".to_string() });
    sys.run().unwrap();
}
```

`AsyncContext` adds methods like `notify_after` and `wait`. These let you schedule future work without blocking the actor. The `GreetTask` is just another message type. The actor processes it when the timer fires.

Convention aside: keep `AsyncContext` for actors that do I/O. Use `Context` for CPU-bound logic or actors that only forward messages. `AsyncContext` has slightly more overhead because it integrates with the async runtime. Pick the context type that matches your workload.

Actors scale by isolation, not by locking. If you find yourself adding `Mutex` inside an actor, you are fighting the model. Split the state into smaller actors instead.

## Pitfalls and compiler errors

Actors solve many concurrency problems, but they introduce new ones. Here are the common traps.

**Message cycles cause deadlocks.** If Actor A sends a message to Actor B, and Actor B waits for a reply from Actor A before processing, the system deadlocks. Actors process messages sequentially. If B is waiting for A, and A is waiting for B, neither moves. Design your message flow as a directed acyclic graph. Avoid circular dependencies.

**Mailboxes overflow silently.** Actors have finite mailboxes. If you send messages faster than the actor can process them, the mailbox fills up. `do_send` drops the message when the mailbox is full. You lose data without an error. Check the return value of `do_send` if message loss is unacceptable, or use `send` to get backpressure.

**Messages must be Send.** Actors often run on different threads. Messages crossing thread boundaries must implement `Send`. If you try to send a message with non-Send data, the compiler rejects you with E0277 (trait bound not satisfied). This usually happens when you include `Rc<T>` or `&str` in a message. Use `Arc<T>` and `String` instead.

**Address cloning is cheap, but actors are not.** Cloning an actor address is just copying a pointer. Spawning an actor allocates a mailbox and schedules work. Do not spawn actors in tight loops. Reuse addresses or batch messages.

**Handle panics kill the actor.** If `handle` panics, the actor stops. The runtime does not restart it automatically. You lose the state and the address becomes invalid. Use `catch_unwind` inside handlers if you need resilience, or implement supervision with a parent actor that monitors children.

Treat the mailbox as a contract. If you send a message, the actor must handle it. If the actor dies, the sender needs to know.

## Decision: when to use actors

The actor model is powerful, but it is not the only tool. Choose the right abstraction for your problem.

Use `actix` when you need a full-featured actor system with supervision, lifecycle management, and integration with the `actix-web` ecosystem. Use `xtra` when you want a lightweight, zero-dependency actor layer on top of `tokio` without the overhead of a separate runtime. Use `tokio::sync::mpsc` channels when your logic is a simple pipeline and you do not need named addresses or complex message routing. Use `Arc<Mutex<T>>` when you have read-heavy shared state and contention is low; actors add indirection that can hurt performance for simple data access.

Counter-intuitive but true: the more you use actors, the harder debugging becomes. Distributed logic is harder to trace than centralized state. Use actors for isolation and scalability, not just because they are trendy.

## Where to go next

- [Thread Safety in Rust vs C++ and Java](/en/thread-safety-in-rust-vs-c-and-java/)
- [How to implement a thread pool](/en/how-to-implement-a-thread-pool/)
- [How to use Arc RwLock for shared mutable state](/en/how-to-use-arc-rwlock-for-shared-mutable-state/)