What Is the Difference Between Rc<T> and Arc<T>?

`Rc<T>` is a reference-counted pointer for single-threaded contexts, while `Arc<T>` (Atomic Reference Counted) is its thread-safe counterpart designed for sharing data across multiple threads.

Shared ownership: Rc versus Arc

You are building a chat client. The message history lives in one place, but the main chat window, the search sidebar, and the notification badge all need to read it. You try to pass the Vec<Message> to the sidebar, and the compiler yells that you moved the value. You try to clone the whole vector, and suddenly you are copying megabytes of text just to display a preview. You need multiple owners. Rust gives you two tools for this: Rc<T> and Arc<T>. They look almost identical. They do the same job. The difference decides whether your app stays fast or crashes in production.

Reference counting in plain words

Both types use reference counting. Wrap a value in Rc or Arc, and Rust puts it on the heap with a counter. Every time you create a new handle to the data, the counter goes up. Every time a handle drops, the counter goes down. When the counter hits zero, the data is deleted.

The difference is how the counter updates. Rc<T> uses a plain integer. It is fast. It assumes only one thread touches it. Arc<T> uses an atomic integer. It is slightly slower, but it guarantees the counter stays correct even if ten threads try to clone or drop the pointer at the exact same nanosecond.

Think of Rc as a paper logbook in a single office. Updating it is instant. Think of Arc as a synchronized digital counter on a billboard. Updating it requires a handshake with the hardware to ensure everyone sees the same number. That handshake costs cycles. Rc skips the handshake because it knows no one else is looking. Arc pays the cost because it cannot make that assumption.

Minimal example

use std::rc::Rc;

/// Demonstrates shared ownership with Rc in a single thread.
fn main() {
    // Rc puts the String on the heap and starts the counter at 1.
    let data = Rc::new(String::from("Shared config"));

    // Convention: Use Rc::clone(&data) instead of data.clone().
    // Both compile and work. Rc::clone makes it obvious you are bumping
    // the reference count, not deep-copying the string.
    let handle_a = Rc::clone(&data);
    let handle_b = Rc::clone(&data);

    // Counter is now 3: original plus two clones.
    assert_eq!(Rc::strong_count(&data), 3);

    // handle_b drops here. Counter goes to 2.
    // Data stays alive because handle_a and data still exist.
}
// data and handle_a drop. Counter hits 0. String is freed.

What happens under the hood

When you call Rc::new, Rust allocates memory for your value and a counter. The counter starts at one. Rc::clone does not copy the value. It allocates a tiny pointer structure and increments the counter. This is cheap. Even for a gigabyte file, cloning an Rc takes the same time as cloning an Rc for a single byte. The cost is in the pointer allocation, not the size of the data.

When the last Rc goes out of scope, the destructor runs. It decrements the counter. If the counter is zero, the destructor frees the heap memory and drops the value. If the counter is not zero, the destructor returns. The data stays alive for the remaining owners.

Arc follows the exact same lifecycle. The only difference is the counter implementation. Arc uses AtomicUsize internally. Every increment and decrement involves atomic instructions. These instructions ensure memory visibility across cores. On some architectures, they flush caches or stall pipelines. The overhead is small per operation, but it adds up in tight loops.

The atomic tax

Arc is not free. The atomic operations required for thread safety introduce measurable overhead. In a single-threaded benchmark, Rc cloning can be five to ten times faster than Arc cloning. The exact ratio depends on the CPU architecture and memory ordering requirements.

The cost comes from memory barriers. Atomic operations must guarantee that writes are visible to other threads in a consistent order. Rc assumes no other threads exist, so it uses plain loads and stores. The compiler can optimize these aggressively. Arc forces the compiler to respect atomic semantics, which limits optimization opportunities.

Do not use Arc in a hot loop on a single thread just because you copied code from a multi-threaded example. The atomic tax is real. Profile before you optimize, but start with Rc unless you have a reason to believe threads are involved.

Mutating shared data

Neither Rc nor Arc provides interior mutability by default. Both types give you immutable references to the inner data. You cannot get a mutable reference from multiple handles. Rust enforces this at compile time.

If you need to change the data after creation, you must wrap the inner type in a mutability helper. For Rc, use RefCell<T>. For Arc, use Mutex<T> or RwLock<T>.

RefCell<T> performs borrow checking at runtime. It tracks how many immutable and mutable borrows exist. If you violate the borrowing rules, RefCell panics. This is safe because the panic prevents data races within the thread. RefCell has no kernel overhead. It is fast for single-threaded mutation.

Mutex<T> uses a lock to protect the data. Only one thread can hold the lock at a time. Other threads block until the lock is released. Mutex is required for Arc because RefCell is not thread-safe. Mutex involves kernel syscalls on some platforms if contention is high, though Rust's implementation uses futexes to minimize this cost.

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

/// Shared mutable state in a single thread.
fn main() {
    // Rc<RefCell<T>> allows mutation through any clone.
    let counter = Rc::new(RefCell::new(0));

    let c1 = Rc::clone(&counter);
    let c2 = Rc::clone(&counter);

    // Borrow mutably through c1.
    *c1.borrow_mut() += 1;

    // Borrow mutably through c2.
    *c2.borrow_mut() += 10;

    // Read through the original.
    println!("Value: {}", *counter.borrow()); // Prints 11
}

Realistic example: Graph structures

Graphs are the classic use case for shared ownership. Nodes often have multiple parents. A single node can be reached via different paths. You cannot represent this with unique ownership. You need Rc to share nodes.

use std::rc::Rc;

/// A node in a graph that can be shared by multiple parents.
#[derive(Debug)]
struct Node {
    label: String,
    // Children are shared. A child can belong to multiple parents.
    children: Vec<Rc<Node>>,
}

fn main() {
    // Create a shared node.
    let shared_node = Rc::new(Node {
        label: "Root".to_string(),
        children: vec![],
    });

    // Two different parents point to the same child.
    let parent_a = Rc::new(Node {
        label: "Parent A".to_string(),
        children: vec![Rc::clone(&shared_node)],
    });

    let parent_b = Rc::new(Node {
        label: "Parent B".to_string(),
        children: vec![Rc::clone(&shared_node)],
    });

    // The shared node has a ref count of 3:
    // 1 from shared_node variable, 1 from parent_a, 1 from parent_b.
    println!("Ref count: {}", Rc::strong_count(&shared_node));
}

Graphs love Rc. Trees usually do not need it unless nodes are shared. Pick the structure that matches your data, not the other way around.

Pitfalls and compiler errors

The compiler protects you from using Rc across threads. Rc does not implement Send. If you try to move an Rc into a thread::spawn, you get E0277 (trait bound not satisfied). The error message tells you that Rc<T> cannot be sent between threads safely. This is a feature. It forces you to choose Arc when you cross the thread boundary.

Reference cycles are the silent killer. If Node A holds an Rc to Node B, and Node B holds an Rc to Node A, the counter never hits zero. The memory leaks forever. Use Weak<T> for back-references to break cycles. Weak pointers do not increment the strong count. They only keep the memory alive if a strong Rc exists.

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

struct Parent {
    children: Vec<Rc<Child>>,
}

struct Child {
    // Weak back-reference. Does not keep parent alive.
    parent: Weak<Parent>,
}

fn main() {
    let parent = Rc::new(Parent { children: vec![] });
    
    // Upgrade Weak to Rc temporarily to access data.
    // Returns None if the parent was dropped.
    let child = Child {
        parent: Rc::downgrade(&parent),
    };

    if let Some(p) = child.parent.upgrade() {
        println!("Parent still alive: {:?}", p);
    }
}

Reference cycles leak memory. If your graph has parent pointers, use Weak. If you do not, you are leaking.

Decision matrix

Use Rc<T> when you need shared ownership within a single thread and performance matters. Use Rc<T> for UI state trees, parser ASTs, or graph structures where nodes are shared but never touch threads.

Use Arc<T> when data must be shared across multiple threads. Use Arc<T> for configuration loaded at startup and read by worker threads, or for message queues where producers and consumers share a buffer.

Use Rc<RefCell<T>> when you need shared ownership and interior mutability in a single thread. This is the go-to for mutable graphs or state machines that update frequently.

Use Arc<Mutex<T>> or Arc<RwLock<T>> when you need shared ownership and interior mutability across threads. Arc handles the pointer safety. The lock handles the data safety.

Start with Rc. Only reach for Arc when the compiler forces you to. The atomic overhead is real, and you do not want to pay for thread safety you are not using.

Where to go next