Box, Rc, Arc: Choosing the Right Smart Pointer
Photo: Unsplash
The first time I needed to put something on the heap in Rust, I reached for Box because it was the one I had heard of. The second time, a recursive type, Box was actually required. The third time I needed two parts of my program to share the same value, Box would not do it, and I went hunting for the alternative. That hunt is where most people meet Rc and Arc and then promptly get confused about which to use.
The good news is the decision is almost mechanical once you ask two questions in order: do I need shared ownership, and if so, do I share across threads. This post walks through those questions with the trade-offs that actually matter.
Box: one owner, on the heap
Box<T> is the simplest smart pointer. It puts a value on the heap and gives you a pointer to it, while preserving Rust's single-ownership rule. When the box goes out of scope, the heap value is freed. No reference counting, no runtime bookkeeping beyond the allocation itself.
You reach for Box in three main situations. First, when a value is large and you want to move a pointer rather than copy bytes. Second, when you need a trait object like Box<dyn Error>. Third, when a type is recursive and the compiler cannot size it without indirection.
// A recursive type cannot exist without indirection:
// the compiler can't compute its size otherwise.
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Nil))));
// each Cons owns the next node exclusively
if let Cons(head, _) = &list {
println!("head is {head}");
}
}The Rust Book's chapter on smart pointers uses this exact cons-list example because it cleanly shows why indirection is sometimes structurally necessary, not just a performance choice. The key property to remember: Box is still single ownership. Exactly one variable owns the boxed value at a time.
Rc: shared ownership on one thread
The moment you need two owners, Box is the wrong tool. Rc<T>, the reference-counted pointer, lets multiple variables co-own the same heap value. It keeps a count of how many owners exist, increments on clone, decrements on drop, and frees the value when the count hits zero.
use std::rc::Rc;
fn main() {
let shared = Rc::new(vec![1, 2, 3]);
let a = Rc::clone(&shared); // count is now 2, no deep copy
let b = Rc::clone(&shared); // count is now 3
println!("count = {}", Rc::strong_count(&shared)); // 3
println!("{:?} {:?}", a.len(), b.len());
} // all three drop, count reaches 0, vec is freedTwo things trip people up here. Rc::clone is cheap; it copies a pointer and bumps a counter, it does not deep-copy the underlying data. And Rc gives you shared reads, not shared mutation. The value behind an Rc is immutable by default. If you need to mutate, you pair it with RefCell for interior mutability, which moves the borrow checking to runtime.
The critical limitation: Rc is not thread-safe. Its counter is a plain integer with no synchronization, so the compiler will refuse to let an Rc cross a thread boundary. This is not a footgun waiting to bite you at runtime; it is a compile error, which is exactly the kind of guardrail Rust is known for.
Arc: shared ownership across threads
When you need shared ownership and the owners live on different threads, Arc<T> is the answer. The "A" stands for atomic: its reference count uses atomic operations so that increments and decrements from multiple threads do not race.
use std::sync::Arc;
use std::thread;
fn main() {
let config = Arc::new(vec!["fast", "safe", "concurrent"]);
let mut handles = vec![];
for id in 0..3 {
let config = Arc::clone(&config); // each thread gets an owner
handles.push(thread::spawn(move || {
println!("thread {id} sees {} flags", config.len());
}));
}
for h in handles {
h.join().unwrap();
}
}The standard library docs for Arc spell out the cost: those atomic operations are more expensive than the plain increments Rc uses. On hot paths with heavy cloning, that overhead is measurable. This is why Rust gives you both types instead of just making everything atomic by default. You pay for thread safety only when you need it.
Like Rc, Arc alone gives shared reads. For shared mutable state across threads, you wrap the inner value in a Mutex or RwLock, giving the common Arc<Mutex<T>> pattern.
Rule of thumb: start with the most restrictive option that compiles. Box if one owner is enough. Rc if you need sharing on a single thread. Arc only when ownership genuinely crosses threads. Each step up the ladder adds runtime cost, so do not climb higher than your actual requirement.
A quick decision flow
When I am unsure, I answer three questions in order:
- Do I need more than one owner of this value? If no, use
Box(or just keep it on the stack). Single ownership is cheapest and clearest. - Will those owners live on different threads? If no,
Rcis enough and avoids atomic overhead. If yes,Arc. - Do the owners need to mutate the shared value? If yes, add interior mutability:
RefCellunderRc, orMutex/RwLockunderArc.
Notice that none of these questions is about performance micro-optimization. They are about the shape of ownership in your program. Get the shape right and the type falls out almost automatically.
One more guardrail worth knowing: reference counting can leak memory if you create a cycle, where two Rc values point at each other so the count never reaches zero. The standard fix is Weak, a non-owning reference that does not keep the value alive. You rarely need it, but when you build something like a tree with parent pointers, that is the tool.
Takeaways
Box<T>is single ownership on the heap. Use it for recursion, trait objects, and moving large values cheaply.Rc<T>enables multiple owners on a single thread with a non-atomic counter. Cloning is cheap and copies no data.Arc<T>is the thread-safe version, paying for atomic reference counting only when ownership crosses threads.- All three give shared reads; for shared mutation add
RefCell(single-thread) orMutex/RwLock(multi-thread). - Pick the most restrictive pointer that compiles, and watch for reference cycles, which
Weakis designed to break.

