Testing Concurrent Rust Code With Loom Interleavings
Stress tests find concurrency bugs by luck. Loom explores every legal ordering of your threads and memory operations, so a missing Acquire fails deterministically.

Concurrency bugs depend on timing. A test that passes a million times can still hide a race that appears on a different CPU. Loom is a model checker for Rust: it runs your code under every ordering that the memory model allows within a bound. It is how Tokio tests its synchronization primitives. Versions: Rust 1.90, loom 0.7.
What goes wrong without it
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
#[test]
fn publish_data() {
let data = Arc::new(AtomicUsize::new(0));
let ready = Arc::new(AtomicBool::new(false));
let (d, r) = (data.clone(), ready.clone());
let writer = thread::spawn(move || {
d.store(42, Ordering::Relaxed);
r.store(true, Ordering::Relaxed); // bug: should be Release
});
while !ready.load(Ordering::Relaxed) {} // bug: should be Acquire
assert_eq!(data.load(Ordering::Relaxed), 42);
writer.join().unwrap();
}On x86 this nearly always passes, because the hardware orders stores strongly. On ARM or under compiler reordering the reader may see ready before data. A normal test cannot show it.
The loom version
Add a cfg switch so the crate uses loom's types in tests:
[target.'cfg(loom)'.dependencies]
loom = "0.7"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(loom)"] }#[cfg(loom)]
use loom::{sync::{atomic::{AtomicBool, AtomicUsize, Ordering}, Arc}, thread};
#[cfg(not(loom))]
use std::{sync::{atomic::{AtomicBool, AtomicUsize, Ordering}, Arc}, thread};Library code then uses these imports instead of std::sync directly. The test:
#[cfg(loom)]
#[test]
fn publish_data_loom() {
loom::model(|| {
let data = Arc::new(AtomicUsize::new(0));
let ready = Arc::new(AtomicBool::new(false));
let (d, r) = (data.clone(), ready.clone());
let writer = thread::spawn(move || {
d.store(42, Ordering::Relaxed);
r.store(true, Ordering::Relaxed);
});
while !ready.load(Ordering::Relaxed) {
loom::thread::yield_now();
}
assert_eq!(data.load(Ordering::Relaxed), 42);
writer.join().unwrap();
});
}RUSTFLAGS="--cfg loom" cargo test --release publish_data_loomthread 'publish_data_loom' panicked at src/lib.rs:31:9:
assertion `left == right` failed
left: 0
right: 42Loom found an execution where ready is true and data is still 0. Change the stores and loads to Release and Acquire:
r.store(true, Ordering::Release);
while !ready.load(Ordering::Acquire) { loom::thread::yield_now(); }and the model passes, having explored all legal orderings. Spin loops need yield_now(), otherwise loom cannot progress the other thread. Run in --release, since the exploration is CPU heavy.
Types you can model
| std | loom | |---|---| | std::sync::atomic::* | loom::sync::atomic::* | | std::sync::{Mutex, RwLock, Condvar} | loom::sync::* | | std::sync::Arc | loom::sync::Arc | | std::thread::spawn | loom::thread::spawn | | std::cell::UnsafeCell | loom::cell::UnsafeCell (detects data races) |
The loom::cell::UnsafeCell variant has with and with_mut methods that panic if two threads would access the data concurrently without synchronization. That check is the strongest reason to use loom with lock-free code (atomics explained).
Keeping models small
The number of schedules explodes with each thread and each operation. Practical rules:
- Two or three threads, a few operations each.
- Test one primitive at a time: a queue, a latch, a reference count.
- Bound preemptions:
LOOM_MAX_PREEMPTIONS=3 cargo test(the default checks exhaustively within the bound the library sets). - Use
LOOM_LOG=traceto see the failing schedule, andLOOM_CHECKPOINT_FILEto replay it.
What loom does not cover
- Async runtimes. Tokio provides its own loom integration. For application async code use deterministic scheduling tools instead (testing graceful shutdown, paused time).
- Larger systems. Models of whole programs do not finish.
- Platform details. It models the C11 memory model, which is the contract Rust gives you.
- Performance. It says nothing about speed.
When it is worth it
Reach for loom when you write code with atomics, UnsafeCell, custom locks or lock-free structures, where a bug means a rare crash in production. For ordinary mutex-protected state, stress tests and careful design are enough ([Arc<Mutex<T>>](/library/rust-arc-mutex-pattern)). The companion for undefined behavior in unsafe code is Miri (Miri).


