Rust’s borrowing rules state that a shared reference (&T) is immutable — you cannot modify the Data through it. This is a compile-time guarantee that prevents data races and enables safe Concurrency. However, there are legitimate cases where you need to mutate data through a shared Reference. Interior mutability types provide this capability while maintaining safety guarantees.
The core tension: &T promises the caller that the data will not change, but sometimes the data Needs to change in response to operations that only have a shared reference available. Interior Mutability resolves this by moving the mutation check from compile time to runtime (for Single-threaded types) or by using synchronization primitives (for multi-threaded types).
UnsafeCell<T> is the foundation of all interior mutability in Rust. It is the only type in the Standard library that allows you to obtain a mutable reference to its interior through a shared Reference. All other interior mutability types (Cell``RefCell``Mutex``RwLock) are built on Top of UnsafeCell.
use std :: cell :: UnsafeCell ;
fn new (value : i32 ) -> Self {
value : UnsafeCell :: new (value),
unsafe { * self . value . get () }
Creating two mutable references to the same data simultaneously. You are responsible for maintaining The aliasing invariant. Violating this is undefined behavior.The compiler assumes that &T never allows mutation. UnsafeCell is the escape hatch that tells The compiler “I will manage the aliasing rules myself.” Without UnsafeCellIt would be impossible To implement Cell``RefCell``MutexOr any other interior mutability type.
Types containing UnsafeCell are not Sync by default. If you want to make a type containing UnsafeCell thread-safe, you must implement Sync manually with unsafe impl Sync:
use std :: cell :: UnsafeCell ;
use std :: sync :: atomic :: { AtomicIsize , Ordering };
unsafe impl Sync for AtomicCounter {}
fn new (value : i64 ) -> Self {
value : UnsafeCell :: new (value),
This is only sound if you can prove that all accesses to the interior are properly synchronized (e.g., via atomics, locks, or platform-specific memory barriers).
Cell<T> provides interior mutability for Copy types. The value is stored inline (no heap Allocation), and you can only access it by copying:
let counter = Cell :: new ( 0 );
assert_eq! (counter . get (), 42 );
counter . set (counter . get () + 1 );
assert_eq! (counter . get (), 43 );
Method Description new(value)Creates a new Cell containing value get()Returns a copy of the value (requires T: Copy) set(value)Replaces the interior value replace(value)Replaces and returns the old value (any T) take()Replaces with Default::default() and returns old into_inner()Consumes the Cell and returns the inner value
Cell works with non-Copy types for set``replaceAnd takeBut not get:
let cell = Cell :: new ( String :: from ( "hello" ));
// cell.get(); // ERROR: String does not implement Copy
let old = cell . replace ( String :: from ( "world" ));
assert_eq! (old, "hello" );
assert_eq! (cell . take (), "world" );
assert_eq! (cell . take (), "" ); // String::default()
1. Reference Counting:
Rc uses Cell internally for the reference count:
let a = Rc :: new ( Cell :: new ( 42 ));
assert_eq! ( Rc :: strong_count ( & a), 3 );
assert_eq! (b . get (), 100 );
2. Mutable Flags in Immutable Contexts:
fn new (level : log :: Level ) -> Self {
Logger { level : Cell :: new (level) }
fn set_level ( & self , level : log :: Level ) {
fn get_level ( & self ) -> log :: Level {
3. Interior Mutation in Closures:
fn count_calls () -> impl Fn () -> usize {
let counter = Cell :: new ( 0 );
let n = counter . get () + 1 ;
Cell has zero overhead beyond the inline storage. There is no reference counting, no runtime Borrow checking, and no heap allocation. The compiler inlines all Cell operations.
RefCell<T> provides interior mutability for any type T. It tracks borrows at runtime using a Reference count and panics if the borrowing rules are violated:
let data = RefCell :: new ( vec! [ 1 , 2 , 3 ]);
let borrow1 = data . borrow ();
let borrow2 = data . borrow ();
assert_eq! ( * borrow1, vec! [ 1 , 2 , 3 ]);
assert_eq! ( * borrow2, vec! [ 1 , 2 , 3 ]);
// data.borrow_mut(); // PANIC: already borrowed immutably
let mut borrow3 = data . borrow_mut ();
assert_eq! ( * borrow3, vec! [ 1 , 2 , 3 , 4 ]);
Method Returns Description new(value)RefCell<T>Creates a new RefCell borrow()Ref<T>Immutable borrow, panics if mutably borrowed borrow_mut()RefMut<T>Mutable borrow, panics if any borrow exists try_borrow()Result<Ref>Non-panicking immutable borrow try_borrow_mut()Result<RefMut>Non-panicking mutable borrow into_inner()TConsumes the RefCell and returns inner value
Ref<T> and RefMut<T> are RAII guards that track the borrow. When the guard is dropped, the Borrow count is decremented:
let data = RefCell :: new ( String :: from ( "hello" ));
let mut guard = data . borrow_mut ();
guard . push_str ( ", world" );
} // guard dropped here, borrow_mut count decremented
let guard = data . borrow ();
assert_eq! ( * guard, "hello, world" );
RefCell maintains two counters:
┌──────────────────────────────┐
│ ┌────────────┬────────────┐ │
│ │ borrow_count │ borrow_mut │ │
│ └────────────┴────────────┘ │
│ borrow_count > 0: can borrow immutably │
│ borrow_count == 0 && !borrow_mut: can borrow mutably │
│ borrow_mut: cannot borrow at all │
└──────────────────────────────────────────┘
neighbors : RefCell < Vec < & ' a Node <' a >>>,
let a = Node { value : 1 , neighbors : RefCell :: new ( vec! []) };
let b = Node { value : 2 , neighbors : RefCell :: new ( vec! []) };
let c = Node { value : 3 , neighbors : RefCell :: new ( vec! []) };
a . neighbors . borrow_mut () . push ( & b);
a . neighbors . borrow_mut () . push ( & c);
b . neighbors . borrow_mut () . push ( & a);
c . neighbors . borrow_mut () . push ( & a);
assert_eq! (a . neighbors . borrow () . len (), 2 );
assert_eq! (b . neighbors . borrow () . len (), 1 );
Use try_borrow and try_borrow_mut to handle borrow conflicts gracefully:
use std :: cell :: { RefCell , BorrowMutError };
let data = RefCell :: new ( vec! [ 1 , 2 , 3 ]);
let _guard = data . borrow ();
match data . try_borrow_mut () {
Err ( BorrowMutError { .. }) => {
eprintln! ( "cannot borrow mutably — already borrowed immutably" );
Mutex<T> provides mutual exclusion for interior mutability across threads. Only one thread can Access the data at a time:
use std :: sync :: { Arc , Mutex };
let counter = Arc :: new ( Mutex :: new ( 0 ));
let mut handles = vec! [];
let counter = Arc :: clone ( & counter);
handles . push ( thread :: spawn ( move || {
let mut num = counter . lock () . unwrap ();
assert_eq! ( * counter . lock () . unwrap (), 10 );
Property RefCell<T>Mutex<T>Thread safety Single-threaded only Multi-threaded Borrow check Runtime (panic) Runtime (blocking) Overhead Minimal (counters) System call on contention Poisoning No Yes (panic while locked) Send + SyncNeither Both (when T: Send)
RwLock<T> allows multiple concurrent readers or a single exclusive writer:
let lock = RwLock :: new ( 5 );
let r1 = lock . read () . unwrap ();
let r2 = lock . read () . unwrap ();
let mut w = lock . write () . unwrap ();
Need interior mutability?
│ ├── Copy types only? → Cell<T>
│ └── Any type? → RefCell<T>
├── Mostly writes? → Mutex<T>
├── Mostly reads? → RwLock<T>
└── One-time init? → OnceLock<T> / LazyLock<T>
OnceCell stores a value that is initialized at most once. It is useful for lazy initialization and For storing values that are set during construction:
database_url : OnceCell < String >,
database_url : OnceCell :: new (),
fn set_database_url ( & self , url : String ) -> Result <(), String > {
self . database_url . set (url) . map_err ( | _ | "already set" . to_string ())
fn get_database_url ( & self ) -> Option < & String > {
LazyLock initializes the value on first access using a closure:
use std :: collections :: HashMap ;
static GLOBAL_CONFIG : LazyLock < HashMap < String , String >> = LazyLock :: new ( || {
let mut m = HashMap :: new ();
m . insert ( "port" . to_string (), "8080" . to_string ());
m . insert ( "host" . to_string (), "localhost" . to_string ());
let port = GLOBAL_CONFIG . get ( "port" ) . unwrap ();
assert_eq! (port, "8080" );
LazyLock is thread-safe — the initialization closure runs exactly once, even if multiple threads Access the value concurrently.
Type Thread-safe Lazy init Set once OnceCell<T>No No Yes OnceLock<T>Yes No Yes LazyLock<T>Yes Yes Yes
Use OnceCell in single-threaded contexts, OnceLock for thread-safe one-time initialization with Manual set, and LazyLock for thread-safe lazy initialization with a closure.
listeners : RefCell < Vec < Weak < dyn Fn ( i32 )>>>,
listeners : RefCell :: new ( vec! []),
fn subscribe ( & self , listener : Weak < dyn Fn ( i32 )>) {
self . listeners . borrow_mut () . push (listener);
fn emit ( & self , value : i32 ) {
let mut listeners = self . listeners . borrow_mut ();
listeners . retain ( | weak | {
if let Some (listener) = weak . upgrade () {
Graphs with back-references require interior mutability because nodes reference each other in Cycles:
children : RefCell < Vec < Rc < Node >>>,
parent : RefCell < Weak < Node >>,
fn new (value : i32 ) -> Rc < Self > {
children : RefCell :: new ( vec! []),
parent : RefCell :: new ( Weak :: new ()),
fn add_child (parent : & Rc < Node >, child : Rc < Node >) {
child . parent . borrow_mut () . set ( Rc :: downgrade (parent));
parent . children . borrow_mut () . push (child);
use std :: collections :: HashMap ;
cache : RefCell < HashMap < u64 , u64 >>,
cache : RefCell :: new ( HashMap :: new ()),
fn call ( & self , arg : u64 ) -> u64 {
if let Some ( & result) = self . cache . borrow () . get ( & arg) {
let result = ( self . f)(arg);
self . cache . borrow_mut () . insert (arg, result);
Cell has zero runtime overhead. All operations compile to direct memory access. The value is Stored inline within the CellWhich itself has the same size as T.
assert_eq! ( std :: mem :: size_of :: < Cell < u64 >>(), 8 );
assert_eq! ( std :: mem :: size_of :: < Cell <[ u8 ; 1024]>>(), 1024 );
RefCell stores the value inline plus a borrow counter ( 2 bytes on 64-bit). Each borrow() and borrow_mut() increments or decrements the counter. try_borrow variants have the Same cost but return Result instead of panicking.
assert_eq! ( std :: mem :: size_of :: < RefCell < u64 >>(), 16 ); // 8 bytes value + overhead
Mutex has a system-level overhead: on Linux, it uses pthread_mutex_t (40 bytes). Locking is a System call on contention and a single atomic operation when uncontended. Mutex always allocates The value on the heap (it uses alloc::sys::Exclusive::new internally ).
assert_eq! ( std :: mem :: size_of :: < Mutex < u64 >>(), 40 ); // platform-dependent
RwLock is larger than Mutex (48 bytes on Linux) because it must track multiple readers. Read Locks are cheaper than write locks but still involve atomic operations. Write locks are comparable To Mutex locks.
Use Cell<T> when:
T is Copy and you only need simple get/set semanticsYou do not need to hold a reference to the interior value Performance is critical and you want zero overhead Use RefCell<T> when:
T is not Copy (e.g., String``VecCustom structs)You need to borrow the interior value (read or write) through a guard You need dynamic borrow checking with error handling Use Mutex<T> when:
Multiple threads need access to the data The critical section may be held across .await points (use tokio::sync::Mutex) You need poisoning semantics (detecting panics in critical sections) RefCell panics in production. borrow_mut() panics if there is an outstanding immutable borrow. In a long-running service, this crashes the process. Use try_borrow_mut() and handle the error, or restructure your code to avoid overlapping borrows.
Using RefCell across threads. RefCell is not Send or Sync. The compiler prevents cross-thread use, but if you bypass this with unsafeYou will have data races. Use Mutex or RwLock for multi-threaded interior mutability.
Holding Ref guards too long. A Ref or RefMut guard keeps the borrow active until it is dropped. If you store the guard in a struct or return it from a function, the borrow persists, potentially causing later borrow_mut() calls to panic. Drop guards as soon as possible.
Mutex poisoning causing cascading failures. If one thread panics while holding a Mutex the mutex becomes poisoned. Subsequent lock() calls return Err. Use lock().unwrap_or_else(|e| e.into_inner()) if you want to recover from poisoning, but be aware that the data may be in an inconsistent state.
Using std::sync::Mutex in async code. A std::sync::Mutex blocks the OS thread while held. If held across an .await point, it blocks all other async tasks on that thread. Use tokio::sync::Mutex for async contexts, or restructure to drop the lock before awaiting.
Cell with non-Copy types and get(). Cell::get() requires T: Copy. For non-Copy types, use borrow() on a RefCell or replace()/take() on a Cell.
Forgetting that UnsafeCell requires unsafe. Direct access to UnsafeCell::get() returns a raw pointer. Dereferencing it requires unsafe and you must maintain the aliasing invariant manually. Prefer Cell or RefCell unless you are building a custom synchronization primitive.
Overusing interior mutability. Interior mutability should be a deliberate design choice, not a default. If you find yourself wrapping everything in RefCellConsider restructuring your ownership model. Interior mutability hides mutation from the type system, making code harder to reason about.
LazyLock initialization panics. If the initialization closure panics, the LazyLock enters a poisoned state and all subsequent accesses panic. Guard against initialization failures if the closure can fail.
Deadlocks with Mutex and RwLock. Acquiring locks in inconsistent order across threads causes deadlocks. Always define and follow a lock ordering protocol. Use try_lock() with backoff for lock acquisition that can fail gracefully.
When a RefCell is dropped while a Ref or RefMut guard exists, the guard keeps the borrow alive Until it is dropped. This means the RefCell’s destructor runs after the guard is dropped:
let cell = RefCell :: new ( vec! [ 1 , 2 , 3 ]);
let guard = cell . borrow ();
// guard is still active — cell's data is borrowed
// When cell is dropped, the borrow is still tracked
// But since guard holds a reference to cell's data, the drop order is:
// 1. guard is dropped (borrow count decremented)
// 2. cell is dropped (data deallocated)
drop (cell); // ERROR: cannot move out of borrowed content
fn new (name : & str ) -> Self {
* self . count . borrow_mut () += 1 ;
impl Drop for SharedCounter {
// Safe to access count during drop — no other borrows can exist
// because we have &mut self
let final_count = * self . count . borrow ();
println! ( "{} was incremented {} times" , self . name, final_count);
use std :: cell :: { Cell , OnceCell , RefCell };
use std :: sync :: { LazyLock , Mutex , OnceLock , RwLock };
// Cell: Copy types, zero overhead
let cell = Cell :: new ( 42 );
cell . set (cell . get () + 1 );
// RefCell: Any type, runtime borrow checking
let refcell = RefCell :: new ( vec! [ 1 , 2 , 3 ]);
let mut guard = refcell . borrow_mut ();
// OnceCell: Single-threaded one-time init
let once = OnceCell :: new ();
once . set ( "initialized" . to_string ());
assert_eq! (once . get (), Some ( & "initialized" . to_string ()));
// OnceLock: Thread-safe one-time init
let lock = OnceLock :: new ();
assert_eq! ( * lock . get () . unwrap (), 42 );
// LazyLock: Thread-safe lazy init with closure
static CONFIG : LazyLock < String > = LazyLock :: new ( || {
std :: fs :: read_to_string ( "config.toml" ) . unwrap_or_default ()
// Mutex: Thread-safe exclusive access
let mutex = Mutex :: new ( vec! [ 1 , 2 , 3 ]);
let mut guard = mutex . lock () . unwrap ();
// RwLock: Thread-safe multiple readers or one writer
let rwlock = RwLock :: new ( vec! [ 1 , 2 , 3 ]);
let read1 = rwlock . read () . unwrap ();
let read2 = rwlock . read () . unwrap ();
let mut write = rwlock . write () . unwrap ();
Thread-safe interior mutability for Copy types:
let counter = Arc :: new ( Cell :: new ( 0 ));
let c1 = Arc :: clone ( & counter);
let c2 = Arc :: clone ( & counter);
assert_eq! (counter . get (), 2 );
across threads when `T: Copy`And concurrent `get` and `set` operations are safe because `Cell` uses interior mutability — `get` copies the value out and `set` replaces it in a single Operation.RefCell is not SyncSo Arc<RefCell<T>> cannot be shared across threads:
let data = Arc :: new ( RefCell :: new ( vec! [ 1 , 2 , 3 ]));
// std::thread::spawn(move || {
// data.borrow_mut().push(4); // ERROR: RefCell is not Send
let data = Arc :: new ( Mutex :: new ( vec! [ 1 , 2 , 3 ]));
let d1 = Arc :: clone ( & data);
let d2 = Arc :: clone ( & data);
let h1 = std :: thread :: spawn ( move || {
let mut guard = d1 . lock () . unwrap ();
let h2 = std :: thread :: spawn ( move || {
let mut guard = d2 . lock () . unwrap ();
assert_eq! ( * data . lock () . unwrap (), vec! [ 1 , 2 , 3 , 4 , 5 ]);
Serde’s Deserialize often requires interior mutability because deserializers need to mutate their State during parsing:
#[serde(default = "default_port" )]
fn default_port () -> u16 { 8080 }
Serde uses Cell and RefCell internally for tracking state during deserialization. This is one of The reasons why RefCell is common in Rust codebases that do heavy serialization.
let call_count = Cell :: new ( 0 );
call_count . set (call_count . get () + 1 );
assert_eq! ( incrementer (), 1 );
assert_eq! ( incrementer (), 2 );
assert_eq! ( incrementer (), 3 );
assert_eq! (call_count . get (), 3 );
values : RefCell < Vec < i32 >>,
values : RefCell :: new ( vec! []),
fn push ( & self , value : i32 ) {
self . values . borrow_mut () . push (value);
fn snapshot ( & self ) -> Vec < i32 > {
self . values . borrow () . clone ()
graph TD
A[Need to mutate through &T?] --> B{Single-threaded?}
B -->|Yes| C{T is Copy?}
C -->|Yes| D[Cell<T> — zero overhead]
C -->|No| E[RefCell<T> — runtime borrow check]
B -->|No| F{Mostly reads?}
F -->|Yes| G[RwLock<T> — concurrent reads]
F -->|No| H[Mutex<T> — exclusive access]
A --> I{One-time init?}
I -->|Yes, single-threaded| J[OnceCell<T>]
I -->|Yes, multi-threaded| K[OnceLock<T> or LazyLock<T>]
I -->|No| B This topic covers the core concepts of interior mutability, including underlying theory, practical implementation, and key applications.
Key concepts include:
ownership, borrowing, and lifetimes structs, enums, and pattern matching traits and generics error handling (Result, Option) concurrency with threads and async Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
## Intuition
Interior mutability lets you modify data through a shared reference, bypassing the usual borrowing rules at runtime. RefCell performs borrow checking at runtime, panicking on violations. Cell provides copyable values without borrow checks. Mutex and RwLock enable thread-safe interior mutability. This pattern is essential for building safe abstractions like caches, lazy initialization, and reference-counted shared state where compile-time checking is too restrictive.
[[rust/02-ownership-borrowing/ownership]] - Compile-time borrow checking rules [[rust/02-ownership-borrowing/lifetimes]] - Lifetime constraints on mutable references [[rust/06-concurrency/concurrency]] - Thread-safe mutability patterns [[rust/03-structs-enums/structs-and-enums]] - Structs with RefCell fields