How to Use Rc<T> for Shared Ownership in Rust

Rc<T> is Rust's single-threaded shared-ownership pointer. Learn how reference counting works, when to combine it with RefCell, how to avoid cycles, and when Arc is the right call instead.

When one owner isn't enough

Rust's ownership rule says every value has exactly one owner. The owner is responsible for cleaning the value up when its scope ends. That rule keeps the language safe and fast. It also occasionally gets in your way.

Picture a graph. Several nodes need to know about a shared list of categories. Or a parser where multiple syntax tree branches all reference the same source string. Or a UI where a piece of state is read by three different views. There's no single "main" owner. Several places need to be able to read the same data, and the data should stay alive as long as anyone is still using it.

That's what Rc<T> is for. The name stands for "reference counted." Think of it like a library card on the inside cover of a book: every time someone borrows the book, the librarian writes a tally mark. When someone returns the book, the librarian crosses one out. When the last mark gets crossed off, the book goes back on the shelf and the storage is reclaimed.

How it actually works

Rc::new(value) puts your value on the heap, alongside a counter that starts at one. When you call Rc::clone, you don't copy the value. You bump the counter and hand back another Rc pointing at the same heap allocation. Each Rc that goes out of scope decrements the counter. When the counter hits zero, the value gets dropped and the memory is freed.

use std::rc::Rc;

fn main() {
    // The String goes on the heap. The Rc wrapper holds a pointer plus a counter
    // that starts at 1.
    let data = Rc::new(String::from("shared"));

    // Rc::clone increments the counter to 2. It does NOT clone the String itself.
    // Both `data` and `clone` point to the same underlying String.
    let clone = Rc::clone(&data);

    // strong_count tells us how many Rc handles currently exist.
    println!("Count: {}", Rc::strong_count(&data));   // prints 2

    // When `clone` goes out of scope at the end of main, count drops to 1.
    // When `data` goes out of scope, count drops to 0 and the String is freed.
}

You'll notice we wrote Rc::clone(&data) instead of data.clone(). Both compile. They're equivalent. The convention is to use the explicit form because data.clone() looks like a deep clone (which would copy the entire String) but it isn't. Writing Rc::clone(&data) makes the intent obvious to anyone reading the code: this is a cheap pointer copy, not a full duplicate. It's a small lie of habit that pays off when you skim diffs.

What you cannot do with Rc

Two big restrictions, and they're both important.

First, Rc<T> is not thread-safe. The counter is a plain integer, not an atomic. If two threads tried to clone or drop an Rc at the same time, the count could become wrong and you'd get either a use-after-free or a memory leak. Rust prevents this at compile time: Rc<T> doesn't implement Send, so you literally cannot move it to another thread. If you need shared ownership across threads, use Arc<T> (atomic reference count) instead. Same API, slightly slower because the counter increments are atomic.

Second, Rc<T> gives you read-only access. There's no Rc::get_mut_unconditional. If you have multiple Rcs pointing at the same data, you can't get a mutable reference, because if you could, you'd violate Rust's "one mutable borrow at a time" rule. So Rc<T> alone is for shared, immutable data. If you need to mutate something through multiple owners, you wrap a layer of interior mutability inside it: Rc<RefCell<T>> for single-threaded mutation, or Arc<Mutex<T>> across threads.

The compiler error you'll see if you try to mutate is cannot borrow as mutable, as Rcdoes not implementDerefMut``. Don't fight it. Reach for RefCell.

A more realistic example: a shared config

Here's where Rc actually pays off. You have a config struct that several parts of your program need to read. None of them owns it exclusively. The config should stay alive as long as anything is still reading it.

use std::rc::Rc;

// A small config struct. Read-only after construction.
struct Config {
    db_url: String,
    max_connections: u32,
}

// A worker that holds a reference to the shared config.
struct Worker {
    name: String,
    // Each worker has its own Rc handle. The actual Config lives on the heap.
    config: Rc<Config>,
}

impl Worker {
    fn run(&self) {
        // Reading through an Rc is just like reading through a reference.
        // Deref kicks in automatically.
        println!("{}: connecting to {} (max {} connections)",
            self.name, self.config.db_url, self.config.max_connections);
    }
}

fn main() {
    // Build the config once and wrap it.
    let config = Rc::new(Config {
        db_url: String::from("postgres://localhost/app"),
        max_connections: 16,
    });

    // Each worker gets its own handle. Cloning is cheap and doesn't copy the config.
    let alice = Worker { name: "alice".into(), config: Rc::clone(&config) };
    let bob   = Worker { name: "bob".into(),   config: Rc::clone(&config) };

    alice.run();
    bob.run();

    // After construction, count is 3: one for `config`, one for each worker.
    println!("Active references: {}", Rc::strong_count(&config));
}

You could have used &Config references with explicit lifetimes. That works for this tiny example, but lifetimes get awkward when the workers are stored in a vec, passed to other functions, or outlive the original config variable. Rc sidesteps all of that. The config lives on the heap and stays there until nothing is pointing at it.

When sharing means mutating: Rc<RefCell>

Rust's separation between sharing (multiple readers) and mutation (one writer) shows up everywhere. Rc<T> gives you sharing. RefCell<T> gives you mutation tracked at runtime instead of compile time. Wrap one in the other and you can have several owners that all want to mutate the same value.

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

fn main() {
    // The shared list is wrapped twice: RefCell for runtime borrow checking,
    // Rc for shared ownership.
    let list = Rc::new(RefCell::new(vec![1, 2, 3]));

    let writer = Rc::clone(&list);

    // borrow_mut() returns a mutable handle, runtime-checked.
    // If anyone else had an outstanding borrow, this would panic.
    writer.borrow_mut().push(4);

    // borrow() gives a shared (read-only) handle.
    println!("{:?}", list.borrow());   // [1, 2, 3, 4]
}

The "ah-ha" with RefCell is that it pushes the borrow check from compile time to runtime. If you borrow_mut() while a borrow() is still alive, your program panics with the message already borrowed: BorrowMutError. That's a real cost. You traded a compile error for a possible crash. In return, you got the flexibility to mutate through multiple owners. Use it when you genuinely can't structure things to satisfy the static checker, not as a default escape hatch.

Cycles: the one bug Rc can't catch

Reference counts have a famous weakness: cycles. If A holds an Rc to B and B holds an Rc to A, neither count will ever drop to zero, and both values will leak. The leaked memory isn't a security issue, but it's still a real bug.

The standard library gives you Weak<T> to break cycles. A Weak is a non-owning handle: it can be upgraded to a real Rc (returning Option<Rc<T>>) but it doesn't keep the value alive on its own. Pattern: parent owns child via Rc, child holds a Weak back to parent.

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

struct Node {
    name: String,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

That's just enough to sketch the shape. The point: as soon as you build a graph where things point back at each other, plain Rc will leak. Reach for Weak.

Common pitfalls

You tried to use Rc across threads. Compiler says Rc<T> cannot be sent between threads safely, with Send not implemented. The fix: switch to Arc<T>. Same API. About the only difference you'll notice is a tiny performance hit on clone and drop because the counter operations are now atomic.

You tried to mutate through Rc<T>. Compiler says cannot borrow as mutable. The fix: wrap in RefCell (single-thread) or Mutex/RwLock (multi-thread).

You wrote data.clone() and copied the whole inner value. Surprise: works, but slow and probably not what you wanted. The fix: write Rc::clone(&data) to make it obvious you're cloning the handle, not the value.

You forgot a cycle could exist. Symptom: memory keeps growing. The fix: identify the cycle and replace one direction with Weak<T>.

You assumed Rc::strong_count is a guarantee. It's not. By the time you read it, another thread could have... wait, no, Rc is single-thread. Still, treat the count as a debugging aid, not a load-bearing piece of logic.

When to use what

Use Rc<T> when you have a single-threaded program and several parts need to read the same data with no clear single owner. Configuration. Trees. Caches read by multiple components.

Use Arc<T> for the same shape, but across threads. Web servers, parallel work pipelines, anything tokio::spawn.

Use Rc<RefCell<T>> when you also need to mutate, single-threaded. Linked structures, observer patterns where multiple things update shared state.

Use Arc<Mutex<T>> for shared mutable state across threads. The default for "I have data and several tasks need to update it."

If you only need shared read-only access and a single function, plain references with lifetimes are usually simpler and have zero runtime cost. Reach for Rc when the lifetimes get tangled, not before.

Where to go next

Reference counting is one slice of Rust's larger story about ownership. The deeper you go, the more you'll see the same pattern repeated: separate sharing from mutation, and pay for the combination only when you need it.

What is Ownership in Rust and Why Does It Matter?

When should I clone vs borrow

What is the Drop trait