---
title: 'Reference Cycles'
source: 'https://academia.sh/en/courses/ownership/reference-cycles'
course: 'Ownership and the Memory Model'
language: en
updated: '2026-09-15T04:58:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Runs code showing that when two values hold each other through Rc, the strong counter never drops to zero and no record appears in the ledger'
    - 'Proves that a weak reference breaks the reference cycle without raising the strong counter, and that once broken, the ledger produces two records'
    - 'Distinguishes with an example that strong and weak reference counters are tracked independently on the same value'
---

# Reference Cycles

How, when two values hold each other through Rc via RefCell, the strong counter never drops to zero and no record ever appears in the ledger — the course's only leak — is measured. That a weak reference breaks the same reference cycle without raising the counter, and that once broken the ledger produces two records at once, is run; strong and weak counter values are printed.

Across this course's three topics, over thirteen lessons, a value's life always fell into
one of four classes, and a zeroth option was never seen — even `Rc` and `Arc`'s counter,
however many times it was cloned, eventually dropped to zero, sooner or later. This final
lesson measures the one case that breaks that very assumption: if two values, through
`RefCell`, each place an `Rc` inside the other, the strong counter never drops to zero, and
no record ever appears in the ledger. Every end measured across the course's thirteen lessons
had a point that was readable from the source and deterministic; this lesson shows that
determinism's own limit — that an end might never happen at all.

The leak measured in the Go curriculum's Concurrency course was a goroutine that was
expected to stop but never did: a running unit was leaking. Here it is not a running unit
that leaks but a value — the source code itself ends in a completely ordinary way, the
program terminates, but two `Node` values remain in memory after that end, never dropped.

## When Two Values Hold Each Other

The `Ledger`, `Trace`, and `Drop` implementation from the core is used here with the same
excerpt. On top of it, exactly one new type is added: `Node` carries its own `Trace` and a
`next: RefCell<Option<Rc<Node>>>` field that holds a strong reference to the next node. Two
`Node` values (`a`, `b`) are built with `Rc::new`, then each gives the other a reference
with `Rc::clone` — the very copying operation measured in the Reference Counting lesson,
applied here only in both directions at once.

**SO13.** — the claim that "when two values hold each other, the strong counter never drops
to zero even after the scope closes" is confirmed by the `ledger length once the scope closes:
0` line in the run below.

```rust
// cycle_formation.rs — two values holding each other: the counter never drops to zero, the ledger stays empty
use std::cell::RefCell;
use std::rc::Rc;

type Ledger = Rc<RefCell<Vec<String>>>;

struct Trace {
    name: String,
    ledger: Ledger,
}

impl Trace {
    fn new(name: &str, ledger: &Ledger) -> Trace {
        Trace { name: name.to_string(), ledger: Rc::clone(ledger) }
    }
}

impl Drop for Trace {
    fn drop(&mut self) {
        self.ledger.borrow_mut().push(self.name.clone());
    }
}

struct Node {
    trace: Trace,
    next: RefCell<Option<Rc<Node>>>,
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(Vec::new()));

    {
        let a = Rc::new(Node {
            trace: Trace::new("node-a", &ledger),
            next: RefCell::new(None),
        });
        let b = Rc::new(Node {
            trace: Trace::new("node-b", &ledger),
            next: RefCell::new(None),
        });

        *a.next.borrow_mut() = Some(Rc::clone(&b));
        *b.next.borrow_mut() = Some(Rc::clone(&a));

        println!("a name: {}, counter: {}", a.trace.name, Rc::strong_count(&a));
        println!("b name: {}, counter: {}", b.trace.name, Rc::strong_count(&b));
    }

    println!("ledger length once the scope closes: {}", ledger.borrow().len());
}
```

```
a name: node-a, counter: 2
b name: node-b, counter: 2
ledger length once the scope closes: 0
```

While the container is full, the counter is two in both: `a`'s own binding is one, the
copy held by `b.next` is one; symmetrically for `b`. When the scope closes, the `a` and `b`
bindings end in the reverse of declaration order — the rule built in the Stack and Heap
lesson holds here too. But each binding's end only lowers its own counter by one: when `b`
ends, the copy held by `a.next` still keeps `b`'s `Node` alive; when `a` ends, the copy held
by `b.next` keeps `a`'s alive too. What remains is a counter of one in both — it never
reaches zero. `Node::trace`'s `Drop` is therefore never triggered, because the `Node` itself
is never dropped; the ledger stays empty until the end of the run.

## A Weak Reference Does Not Raise the Counter, It Breaks the Reference Cycle

The way to break this reference cycle is to turn one of the two directions into a **weak**
reference: `Weak<T>` points at the same value as `Rc<T>` does, but never raises the strong
counter. `Node` splits into two fields here — `strong_next` keeps one direction strong,
`weak_previous` keeps the other direction weak. Which direction is weak is a design
decision; it is not an answer the language gives on its own.

**SO14.** — the claim that "a weak reference never raises the strong counter" is confirmed
by the `a name: broken-a, strong: 1, weak: 1` line in the run below: `b` holds a weak
reference to `a`, and `a`'s strong counter is still one.

```rust
// breaking_with_weak.rs — a weak reference does not raise the counter, and it breaks the reference cycle
use std::cell::RefCell;
use std::rc::{Rc, Weak};

type Ledger = Rc<RefCell<Vec<String>>>;

struct Trace {
    name: String,
    ledger: Ledger,
}

impl Trace {
    fn new(name: &str, ledger: &Ledger) -> Trace {
        Trace { name: name.to_string(), ledger: Rc::clone(ledger) }
    }
}

impl Drop for Trace {
    fn drop(&mut self) {
        self.ledger.borrow_mut().push(self.name.clone());
    }
}

struct Node {
    trace: Trace,
    strong_next: RefCell<Option<Rc<Node>>>,
    weak_previous: RefCell<Weak<Node>>,
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(Vec::new()));

    {
        let a = Rc::new(Node {
            trace: Trace::new("broken-a", &ledger),
            strong_next: RefCell::new(None),
            weak_previous: RefCell::new(Weak::new()),
        });
        let b = Rc::new(Node {
            trace: Trace::new("broken-b", &ledger),
            strong_next: RefCell::new(None),
            weak_previous: RefCell::new(Weak::new()),
        });

        *a.strong_next.borrow_mut() = Some(Rc::clone(&b));
        *b.weak_previous.borrow_mut() = Rc::downgrade(&a);

        println!("a name: {}, strong: {}, weak: {}", a.trace.name, Rc::strong_count(&a), Rc::weak_count(&a));
        println!("b name: {}, strong: {}, weak: {}", b.trace.name, Rc::strong_count(&b), Rc::weak_count(&b));

        let recovered = b.weak_previous.borrow().upgrade();
        println!("is the way back from b to a successful: {}", recovered.is_some());
    }

    println!("ledger once the scope closes: [{}]", ledger.borrow().join(", "));
}
```

```
a name: broken-a, strong: 1, weak: 1
b name: broken-b, strong: 2, weak: 0
is the way back from b to a successful: true
ledger once the scope closes: [broken-a, broken-b]
```

`a`'s strong counter stays at one, because only its own binding owns it; `b`'s weak counter
is one, because `b.weak_previous` looks at it but does not own it. `b`'s strong counter, on
the other hand, is two — its own binding, plus the copy held by `a.strong_next`. The
`upgrade()` call confirms that the weak reference still looks at a valid value: it returns
an `Option<Rc<Node>>`, and since `a` is still alive, the result is `Some`.

**SO15.** — the claim that "once this reference cycle is broken, the ledger produces two
records at once" is confirmed by the `ledger: [broken-a, broken-b]` in the same run's last
line.

When the scope closes, `b` ends first and lowers its strong counter from two to one —
because `a.strong_next` still holds a copy, `b`'s `Node` is not dropped yet. When `a` ends,
its own counter drops to zero and it is genuinely dropped; this drop first writes its own
`trace` field (`broken-a`) to the ledger, then, while dropping its own `strong_next` field,
also drops the last strong reference to `b` — `b`'s counter drops to zero too, and `b` is
dropped as well (`broken-b`). The weak reference never enters this chain: no strong counter
changes while `b.weak_previous` is dropped, because it was never counting one to begin
with.

## The Limiting Measurement: The Compiler Does Not See the Reference Cycle

The difference between the two runs above passes without any rejection in the source. Both
`cycle_formation.rs` and `breaking_with_weak.rs` compile with the same kinds of lines:
`Rc::new`, `Rc::clone`, writing to a `RefCell` field — the very operations used since the
Reference Counting lesson (SO5). The Borrow Checker lesson had shown that the checker tests
ownership discipline but does not guarantee the code's correctness; here that same limit
widens one step further: the compiler never notices that two `Rc` values hold each other,
because every `Rc::clone` call is, on its own, a valid operation — a counter increment,
permitted regardless of which value it points at.

**SO16.** — the claim that "source that holds itself in a cycle goes through the exact same
compilation path as source that does not" is confirmed by both runs (`cycle_formation`,
`breaking_with_weak`) compiling without any warning or error.

This is not an exception to the determinism measured across every lesson of the course, it
is that determinism's own limit: the end point is still readable from the source, only this
time what is read is that an end will **never take place**. Breaking a reference cycle is
therefore the writer's responsibility, not the compiler's; choosing which direction is weak
is too — had `a` been chosen to hold `b` weakly instead of `b` holding `a` weakly, the same
break would have happened in the opposite direction, and the result would not have changed.

## Closing Table

The fourteen end points measured across fourteen lessons are gathered below. Each row's
class is written in that lesson's own text; the table does not make a new classification,
it brings together what was scattered.

| Lesson | Measured value | Where the life ends | Limiting measurement |
|---|---|---|---|
| Stack and Heap | `scope-1/2/3` end order | scope end | end order is exactly the reverse of declaration |
| Ownership and Moves | `into-function`, `inner-scope`, `into-struct` | where it was moved | a move is not a copy; a single record in the ledger |
| Borrowing | `borrow-1` (shared + mutable borrow) | scope end | a borrow does not extend a lifetime, a reference cannot outlive its source |
| Slices | `view` (list slice) | scope end | a slice locks not just the range but the whole source |
| The Borrow Checker | `push_and_count` return value | scope end | the checker tests access discipline, not correctness |
| The Lifetime Concept | `r1`, `r2`, `r3` reference range | scope end | two independent ranges need not nest |
| Explicit Lifetime Annotations | `primary_name`, `other_name` | scope end | an annotation is a request; a body that does not keep the promise is rejected |
| Lifetime Elision Rules | `first_three_letters`, `longer_name` | scope end | elision is a convenience; a signature it does not cover requires an explicit annotation |
| References in Structs | `OwningStruct`, `BorrowingStruct` | where it was moved | a struct cannot carry a reference without a lifetime parameter |
| Heap-Allocated Single Ownership | `on-heap-single`, `list-1..3` | with its container | the same type can fall into two different classes; the class comes from the operation |
| Reference Counting | `shared` (Rc) counter: 1, 2, 3, 2 | with its container | what decides the end is not a copy's identity but how many remain |
| Atomic Reference Counting | `shared` (Arc) counter: 1, 2, 3, 2 | with its container | the atomic counter protects not the data, only the counter |
| Interior Mutability | `counter` (RefCell) borrow attempts | with its container | the rule does not disappear, it turns into an `Err` at runtime |
| Reference Cycles | `node-a`, `node-b` (holding each other) | never ended | the compiler does not see it; the same `Rc::clone` builds both the sharing and the leak |

## Summary

- When two values hold each other through `Rc`, the strong counter never drops to zero; no
  record appears in the ledger — the course's only leak.
- A weak reference (`Weak<T>`) points at the same value but never raises the strong
  counter; keeping one direction weak is enough to break the reference cycle.
- Once the reference cycle is broken, both values are dropped when the scope closes and the
  ledger produces two records at once.
- The compiler never rejects this reference cycle; the `Rc::clone` and `RefCell` writing
  goes through the exact same compilation path as non-reciprocal use.
- In the Go curriculum's Concurrency course, what leaked was a running unit; here what leaks
  is a value — neither comes to an end on its own without an external signal.

## Course Wrap-Up

Across fourteen lessons, this course asked a single question: where does a value's life
end? The answer stayed readable from the source throughout — at scope end, where it was
moved, at an explicit call, together with its container, or, as seen in this lesson, never.
The course's first topic, Ownership Rules, built these classes and the borrow checker that
guards them; its second topic, Lifetimes, gave that check a name (lifetime) and said when
it must be written; its third topic, Shared Ownership, carried ownership past singularity
into being countable, shareable, and finally modifiable — but at every step the end point
was still deterministic, until this lesson showed two values holding onto each other.

Across the course's thirteen lessons, one concept passed by name only and was left
undefined: the `Drop` trait. Closures and iterators never appeared in this course at all;
all three share the same question — what a value **can do**, a question separate from how
long it lives — and the next course opens exactly that question.
