---
title: "21.7 Reference cycles and Weak"
description: "The one memory leak Rust doesn't prevent: two Rcs pointing at each other, and how Weak references break the cycle."
url: "https://learnrust.net/chapter-21/reference-cycles-and-weak/"
last_updated: "2026-06-13"
---

# 21.7 Reference cycles and Weak

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](https://learnrust.net/chapter-21/rc/)): 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:

```rust
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 `Rc`s, 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):

```rust
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 `Rc`s cause a memory leak?

<details class="solution">
<summary>Show solution</summary>

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.

</details>

**Question #2**

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

<details class="solution">
<summary>Show solution</summary>

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.

</details>

**Question #3**

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

<details class="solution">
<summary>Show solution</summary>

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.

</details>

That completes the smart-pointer toolkit. The summary and quiz ([21.x](https://learnrust.net/chapter-21/chapter-21-summary-and-quiz/)) 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.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
