What Is the Difference Between Send and Sync Traits?

Send allows transferring ownership across threads, while Sync allows sharing references across threads safely.

The thread boundary problem

You are building a service that processes requests. You have a shared counter tracking active connections. You spawn a worker thread to handle a new request. You pass the counter to the thread. The compiler rejects you with E0277 (trait bound not satisfied). You try again with a different type. The compiler rejects you again. You wrap everything in unsafe to silence the noise. The program runs. Then, under load, two threads update the counter at the exact same nanosecond. The count corrupts. The program crashes.

Rust stops you before the crash. The gatekeepers are Send and Sync. They are not traits you call methods on. They are marker traits that define the rules for moving and sharing data across thread boundaries. Understanding them turns compiler errors from roadblocks into a map of safe concurrency patterns.

Send moves, Sync shares

Send and Sync describe how a type behaves with threads. They are marker traits, meaning they have no methods. They exist only to carry information to the compiler.

Send means a type can be transferred to another thread. Ownership moves. The value travels across the boundary. Once the value is sent, the original thread no longer has it.

Sync means a type can be shared between threads via references. Multiple threads can hold a reference to the same value at the same time. The value stays in place; the access travels.

Think of a signed contract. Send is mailing the contract to a partner. They hold the paper; you do not. Sync is placing the contract in a glass display case in the lobby. Everyone can read it. No one can take it. No one can scribble on it without permission.

Most types you write are both Send and Sync. A String is Send because you can move it to a thread. A String is Sync because you can share a &String reference across threads; reading a string is safe. The trouble starts with types that break these rules.

The minimal example

Here is the difference in action. A String moves fine. An Rc does not.

use std::thread;
use std::rc::Rc;

fn main() {
    // String is Send. We can move ownership into the thread.
    let msg = String::from("Hello from thread");
    thread::spawn(move || {
        // msg is moved here. The main thread no longer owns it.
        println!("{}", msg);
    }).join().unwrap();

    // Rc is not Send. This would fail to compile.
    // Rc uses non-atomic reference counting.
    // Two threads could increment the count simultaneously and lose an update.
    let count = Rc::new(42);
    // thread::spawn(move || {
    //     println!("{}", count);
    // });
}

The Rc example fails because Rc<T> is not Send. The reference count inside Rc is a plain integer. Incrementing and decrementing that integer is not atomic. If two threads try to clone an Rc at the same time, the count can get out of sync. You get a memory leak or a double-free crash. Rust marks Rc as not Send to prevent this.

How the compiler decides

You rarely implement Send or Sync manually. The compiler derives them automatically based on the fields of your type.

If every field in a struct is Send, the struct is Send. If every field is Sync, the struct is Sync. This recursive check walks down the type tree. A Vec<String> is Send because Vec is Send and String is Send.

The magic happens with UnsafeCell. This is the primitive behind interior mutability. UnsafeCell<T> is never Sync. If your type contains an UnsafeCell, directly or indirectly, the compiler marks it as not Sync.

RefCell<T> contains an UnsafeCell. Cell<T> contains an UnsafeCell. Mutex<T> contains an UnsafeCell. So why is Mutex Sync while RefCell is not?

Mutex protects the UnsafeCell with atomic operations and a lock. The compiler sees the atomic guards and allows Sync. RefCell uses runtime checks that are not thread-safe. The compiler sees the raw UnsafeCell exposure and blocks Sync.

Here is the key insight that unlocks everything. Sync is defined in terms of Send.

A type T is Sync if and only if &T is Send.

If you can send a reference to another thread, the type is Sync. This equation makes the rules consistent. Sync does not mean the type itself moves. It means the reference moves. If &T is safe to send, then T is safe to share.

Real-world sharing

In practice, you share state using Arc<T> and synchronization primitives. Arc stands for Atomic Reference Counted. It is the thread-safe version of Rc.

use std::sync::{Arc, Mutex};
use std::thread;

/// Shared state protected by a mutex.
struct SharedCounter {
    count: i32,
}

fn main() {
    // Arc provides shared ownership across threads.
    // Mutex provides safe interior mutability.
    let counter = Arc::new(Mutex::new(SharedCounter { count: 0 }));

    let mut handles = vec![];

    for _ in 0..10 {
        // Clone the Arc, not the data.
        // This bumps the atomic reference count.
        let counter_clone = Arc::clone(&counter);
        
        let handle = thread::spawn(move || {
            // Lock the mutex to get exclusive access.
            // This blocks if another thread holds the lock.
            let mut data = counter_clone.lock().unwrap();
            data.count += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    // Read the final value.
    let final_count = counter.lock().unwrap().count;
    println!("Final count: {}", final_count);
}

Arc is Send and Sync. The reference count uses atomic instructions, so multiple threads can clone and drop Arc instances safely. Mutex is Sync. You can share a &Mutex<T> across threads. The lock ensures only one thread mutates the data at a time.

Convention aside: use Arc::clone(&counter) instead of counter.clone(). Both compile and do the same thing. The explicit form signals to readers that you are cloning the smart pointer, not the underlying data. A deep clone would be expensive and likely wrong here. The explicit call prevents confusion.

Pitfalls and the E0277 wall

The most common error is E0277. You try to spawn a thread or share data, and the compiler says the type does not implement Send or Sync.

error[E0277]: `std::rc::Rc<i32>` cannot be sent between threads safely

This happens when you accidentally capture an Rc in a closure, or put a RefCell in a shared struct. The fix is usually to swap the type. Replace Rc with Arc. Replace RefCell with Mutex or RwLock.

Another trap is raw pointers. *const T and *mut T are neither Send nor Sync. Raw pointers have no safety guarantees. The compiler assumes they are unsafe until you prove otherwise. If you wrap a raw pointer in a safe abstraction, you may need to implement Send and Sync manually.

use std::marker::PhantomData;

/// A wrapper around a raw pointer that is actually safe.
struct SafePointer {
    ptr: *const i32,
    _marker: PhantomData<i32>,
}

// SAFETY:
// 1. The pointer is only created from a valid reference.
// 2. The lifetime of the data is managed by the caller.
// 3. We only read through the pointer; we never write.
unsafe impl Send for SafePointer {}
unsafe impl Sync for SafePointer {}

Treat the // SAFETY: comment as a proof. If you cannot write the invariants, you do not have a proof. Keep unsafe blocks small. The community calls this the minimum unsafe surface. Isolate the unsafety so the rest of the code remains verifiable.

Decision matrix

Use Send when you need to move ownership of a value to a different thread. Use Sync when you need to share a reference to a value across threads without moving it. Reach for Arc<T> when you need shared ownership across threads; it provides atomic reference counting that is both Send and Sync. Reach for Rc<T> when you have shared ownership but stay on a single thread; it is faster than Arc but lacks thread safety. Reach for Mutex<T> or RwLock<T> when you need to mutate shared data; they wrap non-Sync types and make them Sync by enforcing exclusive access. Avoid RefCell<T> in multi-threaded code; it panics on runtime borrow violations and is not Sync.

Trust the borrow checker here. E0277 is not a suggestion. It is a guarantee that your data race cannot happen.

Where to go next