Skip to main content

Preventing Memory Leaks in Rust: Reference Cycles and Weak<T>

Published: October 28, 2025 Updated: August 28, 2026 Larry Qu 7 min read

Rust’s ownership system prevents dangling pointers and double frees — but not memory leaks. The one case where Rust safe code can leak memory is a reference cycle: two or more Rc<T> values pointing to each other, creating a loop that prevents the reference count from ever reaching zero.

Understanding cycles and how to prevent them with Weak<T> is essential for writing correct Rust programs that involve shared ownership.

What Is a Reference Cycle?

When two Rc<T> values reference each other, they create a cycle:

a → b → a → b → ...

Even after a and b go out of scope, their Rc strong counts never reach zero (each holds a reference to the other). The drop destructor is never called. The memory leaks for the lifetime of the process.

Creating a Cycle (And Why It Leaks)

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

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

use List::*;

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, tail) => Some(tail),
            Nil => None,
        }
    }
}

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
    println!("a count before b: {}", Rc::strong_count(&a)); // 1

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
    println!("a count after b:  {}", Rc::strong_count(&a)); // 2
    println!("b count:          {}", Rc::strong_count(&b)); // 1

    // Create the cycle: a's tail now points to b
    if let Some(tail) = a.tail() {
        *tail.borrow_mut() = Rc::clone(&b);
    }

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

    // At end of scope:
    // b drops: strong_count(b) → 1 (a still holds it), memory NOT freed
    // a drops: strong_count(a) → 1 (b still holds it), memory NOT freed
    // LEAK: ~48 bytes per Cons node never reclaimed

    // This would stack overflow (infinite recursion):
    // println!("a next: {:?}", a.tail());
}

After the function returns, both a and b have strong count 1, so neither is freed.

Detecting Cycles

The standard library doesn’t provide cycle detection. In debug builds, you can reason about counts:

use std::rc::Rc;

fn check_for_leak<T>(rc: &Rc<T>, expected_count: usize) {
    let count = Rc::strong_count(rc);
    if count > expected_count {
        eprintln!("Warning: Rc strong_count={} (expected ≤{}), possible cycle",
            count, expected_count);
    }
}

For production use, tools like Valgrind or address sanitizers can detect leaked Rc values.

Breaking Cycles with Weak<T>

Weak<T> is a non-owning reference to an Rc-managed value:

  • It does not increment the strong count
  • It does not prevent the value from being dropped
  • To access the value, you call .upgrade() which returns Option<Rc<T>>

The pattern: owners use Rc<T>, back-references use Weak<T>.

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

fn main() {
    let strong = Rc::new(String::from("hello"));
    let weak: Weak<String> = Rc::downgrade(&strong); // Create weak ref

    println!("strong count: {}", Rc::strong_count(&strong)); // 1
    println!("weak count:   {}", Rc::weak_count(&strong));   // 1

    // Upgrade to access the value
    match weak.upgrade() {
        Some(s) => println!("Value: {}", s), // "hello"
        None    => println!("Dropped"),
    }

    drop(strong); // Strong count → 0, value is freed

    // After drop, upgrade returns None
    match weak.upgrade() {
        Some(_) => println!("Still alive"),
        None    => println!("Value was dropped"), // This prints
    }
}

Cycle-Free Tree with Weak

The canonical example: a tree where parents own children, but children hold non-owning references back to their parents:

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

#[derive(Debug)]
struct Node {
    value: i32,
    parent:   RefCell<Weak<Node>>,           // Non-owning upward link
    children: RefCell<Vec<Rc<Node>>>,        // Owning downward links
}

impl Node {
    fn new(value: i32) -> Rc<Node> {
        Rc::new(Node {
            value,
            parent:   RefCell::new(Weak::new()),
            children: RefCell::new(Vec::new()),
        })
    }

    fn add_child(parent: &Rc<Node>, child: Rc<Node>) {
        *child.parent.borrow_mut() = Rc::downgrade(parent);
        parent.children.borrow_mut().push(child);
    }
}

fn main() {
    let root  = Node::new(1);
    let child = Node::new(2);
    let leaf  = Node::new(3);

    Node::add_child(&root,  Rc::clone(&child));
    Node::add_child(&child, Rc::clone(&leaf));

    // Navigate downward (strong refs)
    for c in root.children.borrow().iter() {
        println!("root child: {}", c.value);
    }

    // Navigate upward (weak refs — must upgrade)
    let parent_of_leaf = leaf.parent.borrow().upgrade();
    println!("leaf parent: {:?}", parent_of_leaf.as_ref().map(|p| p.value)); // Some(2)

    // Reference counts
    println!("root  strong={} weak={}", Rc::strong_count(&root),  Rc::weak_count(&root));  // 1, 0
    println!("child strong={} weak={}", Rc::strong_count(&child), Rc::weak_count(&child)); // 2, 1
    println!("leaf  strong={} weak={}", Rc::strong_count(&leaf),  Rc::weak_count(&leaf));  // 2, 0

    // When `root` is dropped:
    //   root strong → 0 → root freed
    //   child strong → 1 (child var still holds it) → NOT freed yet
    //   leaf strong → 1 (leaf var still holds it) → NOT freed yet
    // When `child` is dropped:
    //   child strong → 0 → child freed
    //   leaf strong → 0 → leaf freed
    // No leaks!
}

Doubly-Linked List

A doubly-linked list needs both forward and backward links — the classic cycle-prone structure:

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

#[derive(Debug)]
struct ListNode<T> {
    value: T,
    next: Option<Rc<RefCell<ListNode<T>>>>,
    prev: Option<Weak<RefCell<ListNode<T>>>>,  // Weak to break cycle
}

impl<T: std::fmt::Debug> ListNode<T> {
    fn new(value: T) -> Rc<RefCell<Self>> {
        Rc::new(RefCell::new(ListNode {
            value,
            next: None,
            prev: None,
        }))
    }
}

struct DoublyLinkedList<T> {
    head: Option<Rc<RefCell<ListNode<T>>>>,
    tail: Weak<RefCell<ListNode<T>>>,
    len: usize,
}

impl<T: std::fmt::Debug + Clone> DoublyLinkedList<T> {
    fn new() -> Self {
        DoublyLinkedList { head: None, tail: Weak::new(), len: 0 }
    }

    fn push_back(&mut self, value: T) {
        let new_node = ListNode::new(value);

        match self.tail.upgrade() {
            Some(old_tail) => {
                new_node.borrow_mut().prev = Some(Rc::downgrade(&old_tail));
                old_tail.borrow_mut().next = Some(Rc::clone(&new_node));
            }
            None => {
                self.head = Some(Rc::clone(&new_node));
            }
        }

        self.tail = Rc::downgrade(&new_node);
        self.len += 1;
    }

    fn iter_forward(&self) -> Vec<T> {
        let mut result = Vec::new();
        let mut current = self.head.clone();
        while let Some(node) = current {
            result.push(node.borrow().value.clone());
            current = node.borrow().next.clone();
        }
        result
    }
}

fn main() {
    let mut list = DoublyLinkedList::new();
    list.push_back(1);
    list.push_back(2);
    list.push_back(3);

    println!("{:?}", list.iter_forward()); // [1, 2, 3]
    println!("Length: {}", list.len);      // 3
}

The Observer Pattern Without Leaks

A common pattern where you need the subject to hold references to observers, but observers might be dropped:

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

trait Listener {
    fn on_change(&self, new_value: i32);
}

struct Counter {
    value: i32,
    listeners: Vec<Weak<dyn Listener>>, // Weak so counter doesn't keep listeners alive
}

impl Counter {
    fn new() -> Self { Counter { value: 0, listeners: Vec::new() } }

    fn add_listener(&mut self, listener: &Rc<dyn Listener>) {
        self.listeners.push(Rc::downgrade(listener));
    }

    fn increment(&mut self) {
        self.value += 1;
        // Remove dropped listeners and notify live ones
        self.listeners.retain(|weak| {
            if let Some(l) = weak.upgrade() {
                l.on_change(self.value);
                true
            } else {
                false
            }
        });
    }
}

struct Logger { name: String }

impl Listener for Logger {
    fn on_change(&self, value: i32) {
        println!("[{}] counter changed to {}", self.name, value);
    }
}

fn main() {
    let mut counter = Counter::new();

    let l1: Rc<dyn Listener> = Rc::new(Logger { name: "L1".to_string() });
    counter.add_listener(&l1);

    {
        let l2: Rc<dyn Listener> = Rc::new(Logger { name: "L2".to_string() });
        counter.add_listener(&l2);
        counter.increment(); // Both listeners notified
    } // l2 dropped here

    counter.increment(); // Only l1 notified, dead l2 weak ref is cleaned up
    println!("Active listeners: {}", counter.listeners.len()); // 1
}

Rules for Cycle-Free Design

  1. Establish ownership direction: decide which node “owns” which. Parents own children; containers own elements.
  2. Back-references use Weak: any reference going “upward” or “backward” in the ownership tree should be Weak.
  3. Question every Rc::clone: if both sides of a relationship use Rc, you likely have a cycle risk.
  4. Strong count should match your mental model: at any point, the strong count equals the number of live owners. If it’s higher than expected, a cycle may exist.

Rc vs Weak Reference Counts

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

fn main() {
    let a = Rc::new(42);
    let b = Rc::clone(&a);      // strong count: 2
    let w = Rc::downgrade(&a);  // weak count: 1, strong count: 2

    println!("strong: {}, weak: {}", Rc::strong_count(&a), Rc::weak_count(&a));
    // strong: 2, weak: 1

    drop(b);
    println!("strong: {}, weak: {}", Rc::strong_count(&a), Rc::weak_count(&a));
    // strong: 1, weak: 1

    drop(a);
    // strong count → 0: value is dropped (even though weak count is still 1)
    println!("upgraded: {:?}", w.upgrade()); // None
}

The weak count tracks how many Weak<T> pointers exist. When strong count hits zero, the value is dropped regardless of weak count. The Weak<T> allocations themselves are cleaned up when weak count also hits zero.

Summary

Pointer Owns value? Prevents drop? Thread-safe? Use for
Rc<T> Yes Yes No Shared ownership, single thread
Weak<T> No No No Back-references, breaking cycles
Arc<T> Yes Yes Yes Shared ownership, multi-thread
Weak<Arc<T>> No No Yes Back-references in async/multi-thread code

The rule: Rc for forward/downward references, Weak for backward/upward references.

Resources

Comments

👍 Was this article helpful?