The invisible contract
You are building a web server. You have a Config struct that holds the port number and a database URL. You want to share this config between the main thread and a background thread that reloads settings. You wrap the config in an Arc, spawn the thread, and move the config inside.
The compiler rejects you.
You stare at the code. Config has two fields. Both fields are standard types. There are no raw pointers. There are no unsafe blocks. The struct looks perfectly benign. Why does the compiler think your config is dangerous?
The answer isn't in the code you wrote. It is in the code you didn't write. Rust is checking for invisible contracts called marker traits. These traits have no methods. They don't add functionality. They are compile-time flags that tell the compiler whether a type is safe to move across threads or safe to share via references. When the compiler rejects your code, it is telling you that one of these flags is missing.
Stickers on the crate
Marker traits are like safety stickers on a shipping crate. The sticker doesn't change what is inside the crate. It doesn't add handles or wheels. It just tells the logistics system: "This crate is safe to move between warehouses."
If the crate contains explosives, the system refuses to let you slap the sticker on. The sticker is a promise. If you lie about the sticker, the logistics system might drop the crate on a truck driver's foot. In Rust, the sticker is the trait. The warehouse is a thread. The promise is thread safety.
The two most important marker traits are Send and Sync.
Send means a type can be transferred to another thread. Ownership moves across the thread boundary. Sync means a type can be shared between threads via a reference. Multiple threads can hold &T at the same time.
A type is Send if it is safe to move. A type is Sync if it is safe to share. These are not runtime checks. The compiler enforces them at compile time. If a type doesn't implement Send, you cannot move it into a thread. If a type doesn't implement Sync, you cannot share a reference to it across threads.
How the compiler checks
Most of the time, you never write impl Send or impl Sync. The compiler does it for you. It uses a structural rule: a struct is Send if all its fields are Send. It is Sync if all its fields are Sync.
The compiler checks recursively. It looks at your struct, then looks at the fields, then looks at the fields of the fields. If everything in the chain is safe, the compiler stamps the struct with the marker traits.
struct Config {
port: u16,
host: String,
}
fn main() {
// Config is automatically Send and Sync.
// The compiler checked u16 and String, found them safe,
// and derived the traits for Config.
let config = Config { port: 8080, host: "localhost".into() };
}
u16 is a primitive. It is Send and Sync. String owns its data on the heap. The heap allocation is exclusive to the String. Moving the String moves the pointer and the ownership. No two threads can access the data simultaneously unless you explicitly share it. String is Send and Sync.
Since both fields are safe, Config is safe. The compiler derives the traits automatically. You don't need to do anything.
This structural rule is powerful. It means you can build complex types from simple safe parts, and the safety propagates upward. You don't have to annotate every struct. The compiler infers the markers from the composition.
The Sync trick
There is a surprising relationship between Send and Sync. It is not just two independent traits. Sync is defined in terms of Send.
A type T is Sync if and only if &T is Send.
This is the definition. Sync doesn't mean the type has synchronization built in. It means you can send a reference to the type to another thread. If you can send a reference, you can share the type. If you can share the type, it is Sync.
This definition explains why Mutex<T> is Sync. A Mutex protects its inner data. You can send a &Mutex<T> to another thread because the mutex ensures only one thread accesses the data at a time. The reference is safe to move. Therefore, Mutex<T> is Sync.
This definition also explains why Cell<T> is not Sync. Cell allows interior mutability. You can mutate the value through a shared reference. If two threads hold &Cell<T>, they can both mutate the value simultaneously. That is a data race. You cannot send a &Cell<T> safely. Therefore, Cell<T> is not Sync.
Counter-intuitive but true: Sync is just a shorthand for "references are sendable." The compiler uses this equivalence to reduce the problem to checking Send.
When stickers break
The structural rule works until you hit a type that breaks the contract. Some types are intentionally not Send or not Sync. These types opt out of the markers because they are unsafe to share or move.
Rc<T> is a classic example. Rc stands for reference counting. It tracks how many owners point to a value. When the count drops to zero, the value is dropped. Rc uses a non-atomic counter for performance. Atomic operations are expensive. Rc assumes single-threaded usage.
If you move an Rc to another thread, the counter might be updated while another thread reads it. The count can become corrupted. The value might be dropped twice, or leaked forever. Rc is not Send.
use std::rc::Rc;
use std::thread;
fn main() {
let data = Rc::new(vec![1, 2, 3]);
let data_clone = Rc::clone(&data);
// This fails to compile.
// Rc is not Send, so it cannot be moved into the thread.
let handle = thread::spawn(move || {
println!("{:?}", data_clone);
});
}
The compiler rejects this with E0277 (trait bound not satisfied), telling you that Rc<Vec<i32>> cannot be sent between threads safely.
The fix is to use Arc<T>. Arc stands for atomic reference counting. It uses atomic operations for the counter. Atomic operations are safe across threads. Arc is Send and Sync.
Convention aside: The community prefers Rc::clone(&data) over data.clone() for reference-counted types. Both compile and both work. The explicit form signals to readers that you are cloning the reference, not the data. data.clone() looks like a deep clone but isn't. The explicit form avoids confusion.
Lying to the compiler
Sometimes the compiler is too conservative. You might have a type that uses raw pointers or unsafe code, but you know it is thread-safe. The compiler doesn't know that. It sees raw pointers and assumes the worst. Raw pointers are not Send or Sync by default.
In these cases, you can implement the marker traits manually. This requires unsafe. You are telling the compiler: "I have verified this type is safe. Trust me."
struct SafeCounter {
count: std::sync::atomic::AtomicU32,
}
// SAFETY: SafeCounter contains only AtomicU32, which is Send and Sync.
// The atomic operations ensure thread safety.
unsafe impl Send for SafeCounter {}
unsafe impl Sync for SafeCounter {}
This code compiles. The compiler accepts your promise.
If you lie, the consequences are undefined behavior. If you mark a type as Send when it isn't, you can get data races, memory corruption, or crashes. The compiler will not save you. You are responsible for the proof.
Treat the SAFETY comment as a proof. If you can't write it, you don't have one.
A common pitfall is wrapping a non-Send type and assuming the wrapper makes it safe. You cannot fix a type by adding fields. Marker traits are structural. If a field is not Send, the struct is not Send. Adding a Mutex field doesn't make the struct Send if another field is not Send. The whole struct is tainted.
use std::rc::Rc;
struct Wrapper {
// Rc is not Send.
data: Rc<String>,
// Mutex is Send, but it doesn't help.
lock: std::sync::Mutex<()>,
}
// Wrapper is not Send because data is not Send.
// The structural rule propagates the lack of Send.
You cannot opt-in by adding safe fields. You can only opt-out by adding unsafe fields. To make Wrapper Send, you must replace Rc with Arc, or use unsafe impl if you have a valid reason.
PhantomData: The remote control
When you implement unsafe traits, you often need to tell the compiler how your type behaves with respect to ownership and borrowing. This is where PhantomData comes in.
PhantomData is a zero-sized type. It carries type parameters without storing data. It influences variance and marker traits. It tells the compiler how to treat your type based on the type parameter.
If you have a raw pointer *mut T, the compiler doesn't know if you own T or if you are borrowing it. PhantomData<T> tells the compiler you own T. PhantomData<&T> tells the compiler you borrow T.
use std::marker::PhantomData;
struct OwnedPtr<T> {
ptr: *mut T,
// PhantomData<T> indicates ownership.
// This makes OwnedPtr<T> Send if T is Send.
_marker: PhantomData<T>,
}
// SAFETY: OwnedPtr owns T. It is Send if T is Send.
unsafe impl<T: Send> Send for OwnedPtr<T> {}
PhantomData is the tool for fine-grained control over marker traits. It lets you implement Send and Sync conditionally based on the type parameter. Without PhantomData, the compiler might drop the type parameter from the trait bounds, leading to incorrect implementations.
Convention aside: Always name the PhantomData field _marker. This signals to readers that the field is unused and exists only for type system purposes. It follows the community convention for marker fields.
Decision matrix
Use #[derive(Send, Sync)] when you want to document intent on a struct that the compiler already allows, or when you need to suppress a warning in a generic context. The derive is redundant for simple structs but makes the contract explicit.
Use unsafe impl Send when you wrap a type that isn't Send but your wrapper adds synchronization, or when you are building a safe abstraction over raw pointers and have proved thread safety. Isolate the unsafe in a small helper.
Use unsafe impl Sync when your type allows safe shared access across threads, such as a wrapper around atomic operations or a read-only resource. Verify that interior mutability is protected.
Use PhantomData when you need to manually control the marker trait behavior of a type, such as telling the compiler a raw pointer is owned or shared. This is essential for generic wrappers.
Reach for Arc<T> when you need to share data across threads and Rc<T> is not an option. Arc provides the atomic reference counting required for thread safety.
Reach for Mutex<T> or RwLock<T> when you have a Send type that isn't Sync and you need to share it via reference across threads. The lock provides the synchronization that makes the type Sync.
Trust the defaults. The compiler is usually right about what's safe. Only override the markers when you have a rigorous proof.