The circle of ownership
You build a graph. Nodes point to neighbors. You wrap everything in Rc so multiple nodes can share data. You run the program. It works. You stop it. The memory usage never drops. Your process quietly eats RAM until the operating system kills it. You did not write a single unsafe block. You did not touch raw pointers. You just created a circle of ownership.
Rust's ownership model guarantees cleanup. Every value has an owner. When the owner goes out of scope, the value drops. Rc<T> breaks that one-to-one rule by allowing multiple owners. It tracks how many owners exist with a counter. When the counter hits zero, the value drops. The system works perfectly until owners start pointing at each other.
Two Rc pointers forming a cycle keep each other alive. Each pointer increments the counter. Neither counter ever reaches zero. The memory stays allocated forever. The compiler cannot catch this. Reference cycles are a runtime problem, not a type error. You have to break the cycle yourself.
How reference counting actually works
Rc<T> stores two numbers on the heap alongside your data. The strong count tracks how many Rc pointers claim ownership. The weak count tracks how many Weak pointers observe the data without owning it. When you create an Rc, the strong count starts at one. The weak count starts at zero.
Calling Rc::clone bumps the strong count. It does not copy the data. It just hands out another ticket to the same heap allocation. When an Rc drops, the strong count decreases. If the strong count hits zero, Rust drops the data immediately. The weak count then drops to zero, and the heap allocation itself is freed.
Weak<T> works differently. It points to the same heap allocation, but it does not bump the strong count. It bumps the weak count instead. This means Weak pointers cannot keep data alive on their own. They are observers. When all Rc owners leave, the data drops. Any remaining Weak pointers become invalid.
Think of a house with a main key and a spare key. The main key owners can live in the house. The spare key owners can peek through the window. If all main key owners move out, the house gets demolished. The spare keys stop working. That is exactly how Rc and Weak interact.
Breaking the cycle with Weak
You fix a reference cycle by replacing one side of the link with Weak. The owning direction stays Rc. The back-reference becomes Weak. The strong count only increases for the forward links. The cycle breaks. Memory clears when the forward owners drop.
use std::cell::RefCell;
use std::rc::{Rc, Weak};
/// A simple linked list node that avoids reference cycles.
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Weak<List>>),
Nil,
}
impl List {
/// Returns a reference to the tail pointer if this node is a Cons.
fn tail(&self) -> Option<&RefCell<Weak<List>>> {
match self {
// Cons carries the next pointer wrapped in RefCell for interior mutability.
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {
// Create a Nil node and immediately downgrade it to Weak.
// This prevents the cycle from forming at initialization.
let a = Rc::new(Cons(5, RefCell::new(Rc::downgrade(&Rc::new(Nil)))));
// Create b, pointing forward to a with a strong Rc reference.
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
// Break the potential cycle by making a's tail point to b as Weak.
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::downgrade(&b);
}
println!("a next item = {:?}", a.tail());
}
The code above builds two nodes. a points to Nil initially. b points to a with a strong reference. Then a's tail is updated to point back to b as a Weak reference. The strong reference chain is b -> a -> Nil. The back link a -> b is weak. When b drops, a's strong count drops to zero. a drops. The Weak pointer in a becomes invalid. Everything cleans up.
You will notice RefCell wrapping the Weak pointer. Rc only gives you shared references. You cannot mutate data behind an Rc without RefCell or Mutex. The borrow_mut() call checks at runtime that no one else is reading the pointer. If they are, the program panics. That is the trade-off for interior mutability.
Convention aside: write Rc::clone(&value) instead of value.clone(). Both compile. Both work. The explicit form signals to readers that you are cloning the pointer, not the data. It prevents the mental model of a deep copy from creeping in.
Trust the strong count. If it never hits zero, your data never drops.
Building a realistic tree structure
Linked lists are simple. Real code usually involves trees or graphs where children need to know their parent. A parent owns its children. Children should not own their parent. That creates a cycle. The standard pattern is Vec<Rc<Node>> for children and RefCell<Weak<Node>> for the parent.
use std::cell::RefCell;
use std::rc::{Rc, Weak};
/// A tree node that tracks its parent without creating a cycle.
#[derive(Debug)]
struct TreeNode {
value: String,
children: Vec<Rc<TreeNode>>,
// Parent is Weak to avoid circular ownership.
// RefCell allows mutation after the Rc is created.
parent: RefCell<Weak<TreeNode>>,
}
impl TreeNode {
/// Creates a new root node with no parent.
fn new(value: &str) -> Rc<TreeNode> {
Rc::new(TreeNode {
value: value.to_string(),
children: Vec::new(),
parent: RefCell::new(Weak::new()),
})
}
/// Attaches a child and sets up the parent back-reference.
fn add_child(&self, child: Rc<TreeNode>) {
// Store the strong reference to the child.
self.children.push(Rc::clone(&child));
// Set the child's parent to a Weak reference of self.
*child.parent.borrow_mut() = Rc::downgrade(self);
}
/// Safely retrieves the parent, if it still exists.
fn get_parent(&self) -> Option<Rc<TreeNode>> {
self.parent.borrow().upgrade()
}
}
fn main() {
let root = TreeNode::new("Root");
let child = TreeNode::new("Child");
root.add_child(child);
// Verify the back-reference works without creating a cycle.
if let Some(parent) = child.get_parent() {
println!("Child's parent is: {}", parent.value);
}
}
The add_child method demonstrates the pattern. The parent pushes a strong Rc into its children vector. The child receives a Weak pointer to the parent. The strong chain flows downward. The weak chain flows upward. When the root drops, the children drop. The weak parent pointers become invalid. The tree vanishes cleanly.
Notice the upgrade() call in get_parent. Weak pointers do not guarantee the data still exists. They only guarantee the data existed when the Weak was created. upgrade() returns Some(Rc<T>) if the strong count is still positive. It returns None if the data already dropped. You must handle both cases. Ignoring the Option causes a panic.
Convention aside: always call Weak::upgrade() at the point of use, not during initialization. Storing an Option<Rc> alongside a Weak defeats the purpose. Let the Weak pointer do the work. Check the result immediately. Move on if it is None.
Treat Weak::upgrade() as a gate. If the gate closes, the data is gone.
What happens when you get it wrong
Reference cycles do not trigger compiler errors. They trigger silent memory leaks. The program runs. The memory grows. The leak only appears in profiling tools or when the process crashes from out-of-memory conditions. You have to design the ownership graph correctly from the start.
The most common mistake is wrapping everything in Rc without thinking about direction. You create a bidirectional link. Both sides hold strong references. The cycle forms. The fix requires auditing every pointer. Ask which direction represents ownership. Make that direction Rc. Make the other direction Weak.
Another mistake is forgetting interior mutability. Rc<T> only provides shared references. You cannot change the data behind it. If you need to update a Weak pointer after creation, you must wrap it in RefCell. Attempting to mutate through an Rc without RefCell triggers E0596 (cannot borrow as mutable, as it is not declared mutable) or E0502 (cannot borrow as mutable because it is also borrowed as immutable). The compiler forces you to choose a mutability strategy.
A third mistake is assuming Weak keeps data alive. It does not. If you drop all Rc owners, the data vanishes. Any code holding only Weak pointers will see None on the next upgrade(). This is a feature, not a bug. It prevents dangling pointers. Rust forces you to handle the absence explicitly.
You will occasionally see E0277 (trait bound not satisfied) when trying to use Weak in places that expect Rc. Weak does not implement Deref to the inner type. You must call upgrade() first. The compiler will not auto-convert them. The types are deliberately separate to prevent accidental ownership claims.
Do not guess at ownership direction. Map it out. Draw arrows. Label them strong or weak. Fix the graph before writing code.
Choosing the right pointer
Use Rc<T> when multiple owners need to read the same data and the data should live as long as any owner exists. Use Weak<T> when you need a back-reference, a cache pointer, or an observer that should not prevent cleanup. Use Arc<T> when the data must cross thread boundaries and you need atomic reference counting. Use Arc<Weak<T>> when you need thread-safe observers that do not keep data alive across threads. Reach for plain references when lifetimes are simple and the data lives in the same scope. Reach for Box<T> when you need exactly one owner but want heap allocation.
Counter-intuitive but true: the more you use Weak, the more you must handle None in your logic. Plan for it.