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.