Leaking Memory in Safe Rust Through Reference Cycles
Safe Rust can leak memory. See how Rc cycles form, how Weak breaks them, how to detect a leak with counts, and why Rust's guarantee excludes leaks.

A common surprise for people who learned that Rust "prevents memory leaks": it does not. It prevents a different class of bugs, and a cycle of reference-counted pointers is legal and leaks. Everything compiles on Rust 1.90 with edition 2024.
A cycle
use std::cell::RefCell;
use std::rc::Rc;
struct Node {
name: &'static str,
other: RefCell<Option<Rc<Node>>>,
}
impl Drop for Node {
fn drop(&mut self) {
println!("dropping {}", self.name);
}
}
fn main() {
let a = Rc::new(Node { name: "a", other: RefCell::new(None) });
let b = Rc::new(Node { name: "b", other: RefCell::new(Some(Rc::clone(&a))) });
*a.other.borrow_mut() = Some(Rc::clone(&b));
println!("a={} b={}", Rc::strong_count(&a), Rc::strong_count(&b));
}a=2 b=2At the end of main, the variables a and b go out of scope, each count falls from 2 to 1, and neither reaches zero, because a holds b and b holds a. No "dropping" line is ever printed: the destructors never run and the memory is not freed until the process exits. The compiler accepted all of it, and that is by design: leaking cannot cause undefined behavior, so it is not prevented.
Break the cycle with Weak
use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct TreeNode {
name: &'static str,
parent: RefCell<Weak<TreeNode>>,
children: RefCell<Vec<Rc<TreeNode>>>,
}
impl Drop for TreeNode {
fn drop(&mut self) {
println!("dropping {}", self.name);
}
}
fn main() {
let root = Rc::new(TreeNode {
name: "root",
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let leaf = Rc::new(TreeNode {
name: "leaf",
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&root);
root.children.borrow_mut().push(Rc::clone(&leaf));
println!("root strong={} weak={}", Rc::strong_count(&root), Rc::weak_count(&root));
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade().map(|p| p.name));
}root strong=1 weak=1
leaf parent = Some("root")
dropping root
dropping leafThe parent owns children with strong pointers; the child refers back with a Weak. Dropping root brings its strong count to zero, which drops the children vector and with it the strong count of leaf. upgrade() returns Some while the parent is alive and None after.
The same bug with Arc and tasks
The cycle appears in async code as a task that holds an Arc of its own owner, or two caches pointing at each other. The cure is the same: Arc::downgrade, or pass ids and look the object up instead of storing a pointer. The check from Arc and Mutex applies: if you must share ownership, decide which direction owns.
Other safe leaks
std::mem::forget(value)skips the destructor.Box::leak(Box::new(x))gives a&'static muton purpose; useful for global config set once at startup, harmful in a loop.- A thread that never terminates, holding its captures.
- Sending into a channel nobody reads, with an unbounded queue.
Detecting leaks
fn debug_counts<T>(rc: &std::rc::Rc<T>) {
println!("strong={} weak={}", std::rc::Rc::strong_count(rc), std::rc::Rc::weak_count(rc));
}Print counts at the end of a test; a value that should be unique but shows 2 is held somewhere. For whole programs, a heap profiler shows live bytes at exit (memory usage and leaks), and Miri reports leaked memory at the end of a run by default.
Prefer designs without cycles
Trees with parent indexes, arenas (Vec<Node> with usize links) and ECS-style ids avoid reference counting entirely, and are faster. Reach for Rc/Weak when you really need shared mutation of a graph. The ownership rules that make this subject confusing are laid out in ownership explained.


