21.7Reference cycles and Weak

Last updated June 13, 2026

Rust's ownership system has prevented an enormous catalog of bugs across this course: use-after-free, double frees, data races, dangling references, all impossible by construction. It's tempting to conclude that Rust prevents every memory bug. It doesn't, and an honest course says so. There is exactly one memory problem the borrow checker can't catch: a reference cycle built with Rc, which can leak memory by keeping values alive that should have been freed. This lesson shows how the leak happens and how Weak references break it.

How a cycle leaks

Recall how Rc frees its value (lesson 21.5): when the reference count hits zero. Now imagine two values that each hold an Rc pointing at the other. Each one's count includes the other's reference. For either to be freed, its count must reach zero, but its count won't reach zero until the other is freed, and the other won't be freed until the first is. Each keeps the other's count above zero forever. Neither is reachable from your code anymore, but neither is freed. That's a memory leak: memory that's allocated, unreachable, and never returned.

Building one requires mutation (to make two existing nodes point at each other), so it uses the Rc<RefCell<T>> combination from last lesson:

use std::rc::Rc;
use std::cell::RefCell;

struct Node {
    value: i32,
    next: RefCell<Option<Rc<Node>>>,
}

fn main() {
    let a = Rc::new(Node { value: 1, next: RefCell::new(None) });
    let b = Rc::new(Node { value: 2, next: RefCell::new(None) });

    *a.next.borrow_mut() = Some(Rc::clone(&b));  // a -> b
    *b.next.borrow_mut() = Some(Rc::clone(&a));  // b -> a   (cycle!)

    println!("a count: {}", Rc::strong_count(&a));
    println!("b count: {}", Rc::strong_count(&b));
}
a count: 2
b count: 2

Each node's count is 2: one reference from the variable (a or b), and one from the other node pointing back. When main ends, a and b (the variables) drop, dropping their counts to 1 each. But each node is still held alive by the other node's pointer, so neither count reaches 0, and neither Node is freed. The two nodes sit on the heap, pointing at each other, unreachable and unreclaimed, until the program exits. You've leaked them.

A leak is bad, but it is not unsafe

Be precise about what just went wrong. Leaked memory is wasted, but it is not corrupted: nothing reads freed memory, nothing is written twice, there's no undefined behavior. Rust still upholds memory safety (no use-after-free, no data races) even in a cycle; what it fails to uphold is "everything gets freed promptly." Leaks are considered safe in Rust precisely because they can't crash or corrupt, only waste. So the guarantee is narrower than "no memory bugs ever", it's "no unsafe memory bugs", and reference cycles live in exactly that gap.

Weak breaks the cycle

The fix is to make one direction of the cycle a non-owning reference. Weak<T> is Rc's companion: a weak reference points at the same value but does not contribute to the strong count. Because it doesn't keep the value alive, a Weak can't, on its own, prevent the value from being freed, and that's exactly the property that breaks cycles.

The trade is that a Weak might point at something that's already been freed, so you can't read through it directly. You call upgrade(), which returns an Option<Rc<T>>: Some if the value is still alive (giving you a real Rc to use), None if it's already gone. The Option (chapter 11) makes the "might be dead" case impossible to ignore.

The classic shape is a tree where a parent owns its children (strong Rcs, the parent keeps the children alive) but each child points back at its parent weakly (a Weak, so the child doesn't keep the parent alive):

use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,        // weak: child -> parent
    children: RefCell<Vec<Rc<Node>>>,   // strong: parent -> children
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);  // weak link upward

    // Follow the weak link back up, safely:
    match leaf.parent.borrow().upgrade() {
        Some(parent) => println!("leaf's parent value is {}", parent.value),
        None => println!("leaf has no living parent"),
    }
}
leaf's parent value is 5

branch owns leaf strongly (it's in branch.children), so the parent keeps the child alive, the normal ownership direction. But leaf.parent is a Weak, created with Rc::downgrade(&branch), so the child does not keep the parent alive. There's no cycle of strong references, so when these go out of scope, both nodes free cleanly. Reading the parent goes through upgrade(), which here returns Some because branch is still alive; had branch already dropped, it would return None, and we'd handle that instead of touching freed memory.

The rule of thumb: in a structure with links in two directions, make the ownership direction strong (Rc) and the back-reference direction weak (Weak). Parents own children; children refer weakly to parents. That asymmetry is what prevents the cycle.

Key insight

Rc strong references express ownership and keep a value alive; Weak references express a non-owning link that doesn't. A reference cycle leaks only when it's a cycle of strong references, each keeping the next alive. Break the cycle by making at least one link weak, and the counts can reach zero again. The art is choosing which direction owns: the side whose lifetime should drive the other's gets the strong reference; the side that merely needs to refer gets the weak one.

Quiz time

Question #1

How does a reference cycle of Rcs cause a memory leak?

Show solution

Each value in the cycle holds an Rc to the next, so each one's strong count includes the others'. An Rc's value is freed only when its count hits zero, but in a cycle every value is kept alive by another value's pointer, so no count ever reaches zero, even after your code drops all its own references. The values become unreachable but are never freed: a leak.

Question #2

Why is a reference-cycle leak considered "safe" even though it's a bug?

Show solution

Because leaked memory is merely wasted, not corrupted: nothing uses freed memory, nothing is double-freed, there's no undefined behavior or crash. Rust's safety guarantee is specifically about unsafe memory bugs (use-after-free, data races, dangling pointers), and a leak isn't one of those. So leaks fall outside the guarantee; they're a correctness/resource problem, not a safety hole.

Question #3

How does Weak<T> break a cycle, and why must you call upgrade() to use one?

Show solution

A Weak<T> points at an Rc's value but does not increase the strong count, so it doesn't keep the value alive, making at least one cycle link non-owning breaks the cycle and lets counts reach zero. Because a Weak doesn't keep its target alive, the target may already be freed, so you can't read through it directly; upgrade() returns Option<Rc<T>> (Some if still alive, None if gone), forcing you to handle the possibly-already-dropped case safely.

That completes the smart-pointer toolkit. The summary and quiz (21.x) pull the three pointers and their two traits together. After it, chapter 22 takes Rc's thread-safe cousin Arc into the world of concurrency, where shared, mutable state is the entire challenge.