Stop Using Arc<Mutex<T>> — There’s a Better Architectural Pattern
Why Shared Mutable State Is Slowing Your Rust Backend (And What to Use Instead)
Stop Using Arc<Mutex<T>> — There’s a Better Architectural Pattern
Why Shared Mutable State Is Slowing Your Rust Backend (And What to Use Instead)

generated by ai
I’m going to say something that might hurt a little:
If your Rust service is full of Arc<Mutex<T>>…
You probably ported your architecture from another language.
And Rust is silently tolerating it.
But it’s not happy.
And neither is your performance.
The Day I Realized I Was Writing “Java in Rust”
Early in one of our async services, we had this everywhere:
use std::sync::{Arc, Mutex};
struct AppState {
counter: usize,
}
let state = Arc::new(Mutex::new(AppState { counter: 0 }));
for _ in 0..10 {
let state = state.clone();
tokio::spawn(async move {
let mut guard = state.lock().unwrap();
guard.counter += 1;
});
}
It worked.
It compiled.
It was “safe”.
But under load?
Latency spikes. Lock contention. Throughput flattening. Weird async stalls.
And the architecture started feeling… wrong.
That’s when it clicked:
Arc<Mutex<T>> is usually a symptom, not a solution.
Why Arc<Mutex<T>> Feels Natural
Because in most languages:
- Threads share memory
- We protect it with locks
- We move on
So when Rust forces you to wrap shared state:
Arc<Mutex<T>>
It feels like:
“Okay fine, that’s the cost of safety.”
But here’s the deeper truth:
Rust’s ownership model isn’t nudging you toward locks.
It’s nudging you away from shared mutable state entirely.
The Real Problem With Arc<Mutex<T>>
Let’s break it down.
Arc = atomic reference counting
Mutex = blocking mutual exclusion
When you combine them:
Arc<Mutex<T>>
You create:
- Shared ownership
- Shared mutation
- Blocking access
- Contention under concurrency
In async Rust (Tokio especially), this gets worse.
Because:
std::sync::Mutex blocks threads.
And if you use:
tokio::sync::Mutex
It doesn’t block threads — but it still serializes access.
Under load, that becomes a bottleneck.
Visualizing the Contention
Imagine this architecture:
┌──────────────┐
│ Task 1 │
└──────┬───────┘
│ lock()
▼
┌─────────┐
│ Mutex │
└─────────┘
▲
│ lock()
┌──────┴───────┐
│ Task 2 │
└──────────────┘
Only one task can proceed.
Even if you have 8 CPU cores.
You just serialized your system.
That’s not scalable architecture.
That’s a traffic jam.
The Better Pattern: Message Passing + Ownership
Instead of sharing mutable state:
Give ownership to one task.
Other tasks send it messages.
This is the Actor-style pattern.
And it aligns perfectly with Rust.
Rewriting the Same Example (The Right Way)
Instead of this:
Arc<Mutex<AppState>>
We do this:
use tokio::sync::mpsc;
struct AppState {
counter: usize,
}
enum Command {
Increment,
Get(tokio::sync::oneshot::Sender<usize>),
}
async fn state_manager(mut rx: mpsc::Receiver<Command>) {
let mut state = AppState { counter: 0 };
while let Some(cmd) = rx.recv().await {
match cmd {
Command::Increment => {
state.counter += 1;
}
Command::Get(reply) => {
let _ = reply.send(state.counter);
}
}
}
}
Spawning it:
let (tx, rx) = mpsc::channel(32);
tokio::spawn(state_manager(rx));
Using it:
tx.send(Command::Increment).await.unwrap();
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
tx.send(Command::Get(resp_tx)).await.unwrap();
let value = resp_rx.await.unwrap();
No locks. No contention. No shared mutation.
Only one owner of state.
Architecture Diagram (Lock-Free Version)
┌───────────────┐
│ Task A │
└──────┬────────┘
│ send()
▼
┌────────────────┐
│ mpsc Channel │
└──────┬─────────┘
▼
┌────────────────┐
│ State Manager │
│ (owns state) │
└────────────────┘
That’s it.
Single ownership. Serialized mutation by design. No mutex.
Why This Is Architecturally Superior
Because:
- Ownership is clear.
- Mutation is localized.
- Concurrency is controlled.
- Contention disappears.
- Deadlocks become impossible.
You move from:
Shared-memory concurrency
To:
Message-driven concurrency
And that’s a fundamental shift.
But What About Performance?
Here’s the surprising part:
Under load, the channel-based model often performs better.
Why?
Because:
- No atomic refcount bump on every clone
- No mutex locking
- No wake contention
- No thread parking
Tokio’s mpsc is optimized with:
- Lock-free fast paths
- Batched wakeups
- Efficient task scheduling
In real services, this reduces tail latency dramatically.
When Arc<Mutex<T>> Is Actually Fine
Let’s be honest.
It’s okay when:
- Contention is extremely low
- Data is rarely mutated
- You are not inside hot async paths
- It’s short-lived
Example:
Arc<Mutex<HashMap<String, String>>>
Used during startup config load?
Fine.
Used inside every request handler under 10k RPS?
Probably not fine.
Real Production Pattern (Scaling It Up)
In larger systems, this evolves into:
- Actor systems
- Command buses
- Event-driven services
- State sharding
Example: Sharded State
Hash(key) % 4
↓
┌───────┬───────┬───────┬───────┐
│Shard 0│Shard 1│Shard 2│Shard 3│
└───────┴───────┴───────┴───────┘
Each shard:
- Owns its own state
- Runs independently
- Has its own channel
Now you have parallelism without locks.
Why Rust Pushes You Toward This
Because Rust’s core philosophy is:
Shared mutable state is dangerous.
The borrow checker enforces:
- Single mutable owner
- Or multiple immutable references
Arc<Mutex<T>> is basically saying:
“Fine, I’ll bypass that rule carefully.”
But the better design is:
Don’t bypass it.
Embrace it.
Code Flow Comparison
Lock-Based
Task → Clone Arc → Lock → Mutate → Unlock
Message-Based
Task → Send Message → State Owner Handles → Done
One requires blocking. One requires coordination. One scales better.
The Emotional Shift
The first time we removed Arc<Mutex<T>> from a core service, something strange happened.
The code became:
- Simpler
- Easier to reason about
- Deadlock-proof
- More predictable under load
And debugging got easier.
Because state lived in one place.
Not everywhere.
Final Truth
Arc<Mutex<T>> is not evil.
But it’s often a sign that your architecture still assumes:
Shared mutable memory across threads is normal.
In Rust, it’s not.
Rust wants you to design around:
Ownership. Isolation. Message passing. Explicit concurrency.
And once you lean into that…
You stop fighting the language.
And your systems start scaling naturally.
메타데이터
- post_id
- cc4d26b89a3e
- slug
- stop-using-arc-mutex-t-theres-a-better-architectural-pattern-cc4d26b89a3e
- url
- https://medium.com/@theopinionatedev/stop-using-arc-mutex-t-theres-a-better-architectural-pattern-cc4d26b89a3e
- canonical_url
- https://medium.com/@theopinionatedev/stop-using-arc-mutex-t-theres-a-better-architectural-pattern-cc4d26b89a3e
- author_url
- https://medium.com/@theopinionatedev
- status
- ok
- fetched_at
- 2026-06-26 12:24:55