Interior Mutability in Rust: Cell, RefCell & Mutex

Max WellsMax WellsFounder of Rustify

TL;DR: Interior mutability is a design pattern that lets you mutate data even when you only hold an immutable (&T) reference. Rust's borrow rules are enforced at compile time by default; interior mutability shifts enforcement to runtime. The main types are: Cell<T> (cheap, single-threaded, Copy values), RefCell<T> (single-threaded, borrow-checked at runtime), and Mutex<T> / RwLock<T> (multi-threaded). Use the simplest type that fits your constraints.


Why Does Interior Mutability Exist?

Some valid programs cannot be expressed under Rust's compile-time borrow rules; interior mutability is the escape hatch.

The classic case: a graph or tree where nodes need to mutate their neighbors, but the borrow checker cannot statically prove the borrows do not overlap. Another case: implementing impl Trait methods that take &self but need to update internal caches or counters.

Rust's type system allows this because Cell, RefCell, and Mutex enforce the aliasing + mutation invariant at runtime rather than compile time; making them sound even if less efficient.


What Is Cell<T>?

Cell<T> provides interior mutability for Copy types with zero runtime overhead; no borrow checking, just direct value swaps.

use std::cell::Cell;
 
struct Counter {
    value: Cell<u32>,
}
 
impl Counter {
    fn increment(&self) {  // takes &self, not &mut self
        self.value.set(self.value.get() + 1);
    }
}

Cell works by copying values in and out; it never gives you a reference to its interior. This is why it only works for Copy types (or with .replace()/.take() for non-Copy).


What Is RefCell<T>?

RefCell<T> provides interior mutability for any type, enforcing Rust's borrow rules at runtime; it panics if you violate them.

use std::cell::RefCell;
 
let data = RefCell::new(vec![1, 2, 3]);
 
{
    let mut v = data.borrow_mut(); // runtime borrow check
    v.push(4);
} // borrow released here
 
println!("{:?}", data.borrow()); // [1, 2, 3, 4]

borrow() returns a Ref<T> (immutable). borrow_mut() returns a RefMut<T> (mutable). If you call borrow_mut() while any borrow is active, it panics at runtime.


What Is Mutex<T>?

Mutex<T> is the thread-safe version of interior mutability; it blocks until the lock is available, and is Send + Sync.

use std::sync::{Arc, Mutex};
 
let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
 
std::thread::spawn(move || {
    *c.lock().unwrap() += 1;
});

RefCell is !Sync; never use it across threads. Use Mutex (exclusive access) or RwLock (multiple readers, one writer) for shared mutable state across threads.


Which Type Should You Use?

TypeThread-safeWorks withCost
Cell<T>Copy typesZero
RefCell<T>Any typeRuntime borrow counter
Mutex<T>Any Send typeOS lock (blocking)
RwLock<T>Any Send + SyncOS lock (readers share)
AtomicUsize etc.Primitives onlyLock-free

Use CellRefCellMutex in that order of preference.


Frequently Asked Questions

No; RefCell is !Sync, so the compiler prevents it from being shared across threads. It can only cause panics (at runtime), not undefined behavior.

The classic single-threaded shared mutable ownership pattern: Rc for multiple owners, RefCell for mutability. For multi-threaded code, use Arc<Mutex<T>> instead.

No. The invariant (no simultaneous aliased mutation) is still enforced; just at runtime. The unsafe keyword is not needed; the types handle it internally.


Sources


  • refcell: Deep dive into RefCell<T>
  • mutex: Thread-safe interior mutability
  • rc: Often paired with RefCell for shared ownership
  • Ownership: Interior mutability is one of Rust's key ownership escape hatches

Keep Reading

Ready to Land a $80-120k Rust Job?