Fearless concurrency: patterns we ship
"Fearless concurrency" is Rust's most-quoted tagline and its most misunderstood. It doesn't mean concurrency is easy. It means a specific, expensive class of bugs — data races — is caught by the compiler instead of by your users at 3 a.m. The fear that's gone is the fear of silent corruption. The engineering judgement is still on you. Here are the patterns we actually reach for.
The compiler is your first reviewer
Rust's ownership rules enforce one simple invariant across threads: you can have many readers or one writer, never both at once. The Send and Sync marker traits encode what's safe to move or share between threads, and the borrow checker refuses to compile code that violates them. A function that tries to mutate shared state from two threads without synchronization simply does not build. That single guarantee removes the entire category of heisenbugs that makes concurrent C++ so dangerous.
Pattern 1: Prefer channels to shared state
The default instinct from other languages is "wrap it in a mutex." It works, but locks invite contention and deadlocks as the system grows. The more robust default in Rust is to share by communicating: give each piece of state a single owner and send it messages.
// One owner of `state`; everyone else sends messages.
let (tx, mut rx) = mpsc::channel::<Command>(1024);
tokio::spawn(async move {
let mut state = State::default(); // owned here, never shared
while let Some(cmd) = rx.recv().await {
state.apply(cmd);
}
});
No lock, no contention, and the ownership is obvious from the types. When you do need shared read-mostly state, Arc<T> for sharing and Arc<Mutex<T>> or RwLock for the rare write are fine — just reach for them second, not first.
Pattern 2: Bounded queues and backpressure
The single most important decision in a concurrent pipeline is what happens when a consumer can't keep up. An unbounded queue answers "grow forever" — which means a slow consumer turns a traffic spike into an out-of-memory crash. Always bound your channels. When the bound is hit you get a choice point: block the producer (apply backpressure) or drop (shed load). Both are valid; the bug is not choosing.
In latency-sensitive systems we usually drop: a stale event is worthless, so we'd rather discard it than let a backlog build and serve nothing on time.
Pattern 3: The actor model falls out naturally
Combine the first two patterns and you have actors: independent tasks that own their state and communicate over bounded channels. Each actor is single-threaded internally — so its logic is simple, no locks — while the system as a whole is massively concurrent. This composes cleanly: a receiver actor, a pool of worker actors, a submitter actor, each with one job and a clear interface. It's how we structure ingestion-to-execution pipelines, and it keeps each piece independently testable.
Pattern 4: Parallelism vs. concurrency — pick deliberately
- CPU-bound work (hashing, parsing, math): use real threads or a pool like
rayon.data.par_iter().map(...)saturates your cores with one line. - I/O-bound work (network, disk): use
async/awaiton a runtime like Tokio. Thousands of concurrent connections, a handful of OS threads.
Mixing them up is a classic mistake — running CPU-heavy work directly on an async runtime starves the executor and tanks your latency. Keep blocking work off the async threads (spawn_blocking or a dedicated pool).
Pattern 5: Make shutdown a first-class feature
Concurrent systems that can't stop cleanly leak resources and corrupt state on restart. Wire a cancellation signal (a broadcast channel or CancellationToken) through every task from the start, and let each actor flush and exit on it. Retro-fitting graceful shutdown is painful; designing for it costs nothing.
The takeaway
Reach for channels before locks, bound every queue and decide block-or-drop on purpose, structure work as single-owner actors, and separate CPU-bound from I/O-bound execution. None of this is exotic — it's just the set of defaults that, combined with the compiler's guarantees, lets a small team ship highly concurrent systems that don't fall over under load.
Need a concurrent system that holds up under real load? That's our default kind of work.