Why Actors Are Perfect for WebSockets

What’s the big deal, why not use Arc and Mutex ?

Actors Are Not Rust-Specific - They're a Universal Pattern!
The Actor Model is Language-Agnostic
The Actor Model was actually invented in 1973 by Carl Hewitt - way before Rust existed! It's a conceptual framework that can be implemented in any language.

The WebSocket Challenge

Imagine you’re building a chat app. Each user has a WebSocket connection, and you need to:

  • Handle messages from each user
  • Broadcast messages to other users
  • Manage user state (online/offline, rooms, etc.)
  • Handle disconnections gracefully

Without Actors (Traditional Approach)

// This gets messy fast!
struct ChatServer {
    users: Arc<Mutex<HashMap<UserId, WebSocket>>>,
    rooms: Arc<Mutex<HashMap<RoomId, Vec<UserId>>>>,
    // ... more shared state with locks
}

// Every operation needs locks:
async fn handle_message(server: Arc<ChatServer>, msg: Message) {
    let mut users = server.users.lock().await;
    let mut rooms = server.rooms.lock().await;
    // Complex lock ordering to avoid deadlocks
    // What if a user disconnects while we're holding locks?
}

With Actors (Clean Approach)

// Each connection gets its own actor
struct UserActor {
    user_id: UserId,
    websocket: WebSocket,
    chat_server: ActorRef<ChatServer>,
}

// Chat server is also an actor
struct ChatServerActor {
    users: HashMap<UserId, ActorRef<UserActor>>,
    rooms: HashMap<RoomId, Vec<UserId>>,
    // No locks needed!
}

Why This Works So Well

1. Natural 1:1 Mapping

  • Each WebSocket connection = One actor
  • Each actor handles one user’s messages sequentially
  • No need to worry about concurrent access to user state

2. Isolation Prevents Chaos

// User A disconnecting can't break User B's connection
// Each actor fails independently
if user_actor_crashes {
    only_that_user_affected();
    other_users_keep_chatting();
}

3. Backpressure Handling

// If User A sends messages too fast:
// - Their actor's mailbox fills up
// - Only THEIR messages get dropped/delayed
// - Other users unaffected

4. Clean Connection Lifecycle

impl UserActor {
    async fn handle_disconnect(&mut self) {
        // Clean up this user's state
        self.chat_server.send(UserDisconnected(self.user_id)).await;
        // Actor dies naturally - no manual cleanup needed
    }
}

The Pattern in Action

WebSocket 1 ←→ UserActor 1 ──┐
                              │
WebSocket 2 ←→ UserActor 2 ──┼──→ ChatServerActor
                              │
WebSocket 3 ←→ UserActor 3 ──┘

Each UserActor processes its WebSocket messages sequentially, then forwards relevant messages to the ChatServerActor, which coordinates between users.

Why Not Just Use Async/Await?

You could, but you’d end up recreating actors manually:

  • Spawning tasks for each connection ✓ (that’s an actor)
  • Using channels for communication ✓ (that’s message passing)
  • Managing task lifecycles ✓ (that’s actor supervision)

Actors just give you a clean framework for patterns you’d build anyway!

The key insight: WebSockets are inherently stateful and concurrent – exactly what actors excel at managing.