Send and Sync in Rust: 'Future is not Send' Explained

Max WellsMax WellsFounder of Rustify

TL;DR: Send means a type can be moved to another thread. Sync means a type can be shared by reference across threads (i.e., &T is Send). Both are auto-traits, the compiler derives them automatically for most types. Types that are neither (Rc<T>, raw pointers) cannot cross thread boundaries. This is how Rust guarantees data-race freedom at compile time: if your code compiles, it's thread-safe.


What Is Send?

Send is a marker trait that says: it is safe to move a value of this type to a different thread.

use std::thread;
 
fn spawn_with<T: Send + 'static>(val: T) {
    thread::spawn(move || {
        // `val` was moved here; to a new OS thread
        println!("running in a new thread");
        drop(val);
    });
}
 
spawn_with(String::from("hello")); // String: Send ✅
spawn_with(42u32);                 // u32: Send ✅
// spawn_with(Rc::new(1));         // Rc<T>: !Send ❌; compile error

Most types are automatically Send. The exceptions are types that rely on thread-local state or non-atomic reference counts, like Rc<T> and raw pointers.


What Is Sync?

Sync is a marker trait that says: it is safe to share a reference to this type across multiple threads simultaneously.

A type T is Sync if &T is Send, meaning a shared reference to T can be sent to another thread without causing data races.

use std::sync::Arc;
use std::thread;
 
let data = Arc::new(42u32); // u32: Sync, so Arc<u32>: Send + Sync
 
let data2 = Arc::clone(&data);
thread::spawn(move || {
    println!("{}", *data2); // multiple threads read simultaneously; safe
});
 
println!("{}", *data); // main thread also reads; safe

Arc<T> is Sync when T: Send + Sync. Mutex<T> is Sync because it serializes access, even if T is not Sync on its own.


Which Common Types Are Not Send or Sync?

Rc<T> and Cell<T>/RefCell<T> are the main !Send/!Sync types you'll encounter. Raw pointers are also !Send + !Sync.

TypeSendSyncReason
Rc<T>Non-atomic ref count, races if shared
RefCell<T>Runtime borrow tracking is not thread-safe
Cell<T>Interior mutability without synchronization
*mut T / *const TRaw pointers, no safety guarantees
Arc<T>✅ (if T: Send+Sync)Atomic ref count
Mutex<T>✅ (if T: Send)Serializes access
MutexGuard<T>✅ (if T: Sync)Must be unlocked on same thread

Why Does the Compiler Say "Future is not Send"?

The most common Send error in async code: a future holds a !Send type across an .await point. The fix is usually replacing Rc with Arc or avoiding holding non-Send guards across awaits.

use std::rc::Rc;
 
// This will NOT compile with tokio::spawn:
async fn broken() {
    let x = Rc::new(42); // Rc is !Send
    some_async_work().await; // x is held across .await
    println!("{x}");
}
 
// Fix: use Arc instead
async fn fixed() {
    let x = std::sync::Arc::new(42); // Arc is Send
    some_async_work().await;
    println!("{x}");
}

Other common causes: holding a MutexGuard across .await (use tokio::sync::Mutex instead), or capturing non-Send closures.


How Do You Implement Send and Sync Manually?

You can implement Send/Sync manually with unsafe impl, only do this when you have verified thread-safety that the compiler can't infer.

use std::sync::Arc;
 
struct MyThreadSafeWrapper(*mut u8); // raw pointer; normally !Send
 
// SAFETY: we guarantee that access to the inner pointer
// is synchronized externally and the pointer is valid
unsafe impl Send for MyThreadSafeWrapper {}
unsafe impl Sync for MyThreadSafeWrapper {}

This is how the standard library itself implements Send/Sync for types like Mutex<T> and Arc<T>. Only reach for unsafe impl when you have proven correctness, it bypasses the compiler's guarantees.


Frequently Asked Questions

The compiler automatically derives Send and Sync for any type where all fields are Send/Sync. This means you get thread safety for free for normal structs, you only need to think about it when your type contains raw pointers or non-atomic shared state.

Yes, if T: Send. Arc provides shared ownership via atomic ref counting (Sync), and Mutex serializes mutable access. Together they're the standard pattern for thread-safe shared mutable state.

'static means the type contains no non-'static references, the spawned task must own all its data (or use Arc). Send means it can be moved to a different worker thread in Tokio's thread pool.

Yes, but it's rare. MutexGuard<T> is Sync (if T: Sync) but not Send, a lock guard must be released on the same thread it was acquired on.


Sources


  • Arc: Arc<T> is Send + Sync when T: Send + Sync, the thread-safe ownership type
  • Mutex: Mutex<T> makes a !Sync type Sync by serializing access
  • Async/Await: tokio::spawn requires futures to be Send + 'static
  • Rc: Rc<T> is !Send + !Sync, the single-threaded alternative to Arc
  • Unsafe: Manual Send/Sync impls require unsafe impl
  • RwLock: RwLock<T> is one of the most common synchronization primitives whose safety depends on Send + Sync
  • once-cell: OnceLock and once_cell matter because their global access patterns still rely on Send + Sync

Keep Reading

Ready to Land a $80-120k Rust Job?