How to Use std::marker Traits (Send, Sync, Copy, Sized)
You are building a multi-threaded web server. You define a Config struct to hold your database URL and a cache size. You wrap it in an Arc and pass it to a worker thread. The compiler rejects you. It says Config cannot be shared between threads. You look at the struct. It contains only a String and a u32. Both seem perfectly safe. Why does the compiler think this is dangerous?
You are hitting the marker traits. They are the invisible rules that govern how your types behave in memory and across threads. They do not add methods. They do not change the layout of your data. They are flags that tell the compiler what operations are allowed on a type. When the compiler checks concurrency or memory safety, it looks at these flags. If the flags are missing or wrong, the code does not compile.
Marker traits live in std::marker. The four you will encounter most often are Send, Sync, Copy, and Sized. Understanding them stops you from fighting the compiler and helps you design types that work with Rust's safety guarantees.
What marker traits actually do
Marker traits are empty. They have no methods. You cannot call value.send() or value.sync(). They exist solely for the compiler to check bounds. When you write a generic function that requires T: Send, the compiler verifies that T implements the Send trait. If it does not, compilation fails.
Think of marker traits like security tags on luggage. The tag does not change the contents of the bag. It just tells the automated scanner whether the bag is allowed on the conveyor belt. Send means the bag can move to a different terminal. Sync means multiple scanners can inspect the bag at the same time. Copy means the bag can be duplicated instantly without unpacking. Sized means the bag fits in the scanner.
The compiler derives these traits automatically for most types. If you define a struct, the compiler checks its fields. If all fields implement a trait, the struct implements it too. This recursive derivation is why your Config struct usually works. If it fails, one of the fields is breaking the rule.
/// A configuration struct with simple fields.
/// The compiler derives Copy because u16 and u32 are Copy.
#[derive(Copy, Clone)]
struct Config {
port: u16,
max_connections: u32,
}
fn main() {
let c = Config { port: 8080, max_connections: 100 };
// Copy happens implicitly. c is still valid.
// If Config were not Copy, this would move c and invalidate the original.
let c2 = c;
println!("Original port: {}", c.port);
println!("Copy port: {}", c2.port);
}
The compiler allows c2 = c to copy the value because Config implements Copy. Without Copy, this assignment would move the value, and using c afterward would trigger E0382 (use of moved value). The marker trait changes the semantics of assignment and function calls.
Send and Sync: concurrency flags
Concurrency in Rust relies on two marker traits: Send and Sync. They control how data moves between threads.
Send means ownership can move across thread boundaries. If T: Send, you can pass a value of type T to another thread using std::thread::spawn or a channel. The value is transferred, and the original thread no longer has access.
Sync means shared references can be accessed from multiple threads. If T: Sync, then &T is Send. This equivalence is the key insight. Sync does not mean the value itself moves. It means you can send a reference to the value to another thread, and that thread can read it safely while other threads also hold references.
The compiler derives Send and Sync based on fields. A struct is Send if all its fields are Send. It is Sync if all its fields are Sync. Most standard library types are both. String, Vec, i32, and Arc are Send + Sync.
Some types are intentionally not Send or Sync. Rc<T> is neither. It uses reference counting without atomic operations. Sharing an Rc across threads causes data races. Cell<T> is Send but not Sync. You can move a Cell to another thread, but you cannot share references to it. Cell allows interior mutability without synchronization. Multiple threads accessing the same Cell would race.
use std::cell::Cell;
use std::sync::Arc;
use std::thread;
/// A counter using interior mutability.
/// Cell is not Sync, so SharedCounter is not Sync.
struct SharedCounter {
count: Cell<u32>,
}
fn main() {
// Arc requires T: Send + Sync.
// SharedCounter is not Sync, so this fails.
let counter = Arc::new(SharedCounter { count: Cell::new(0) });
let _handle = thread::spawn(move || {
// Error: SharedCounter cannot be shared between threads safely.
// The compiler rejects this with E0277 (trait bound not satisfied).
counter.count.set(1);
});
}
The compiler rejects the Arc::new call because Arc requires Send + Sync. SharedCounter fails the Sync check due to Cell. The fix is to use a thread-safe alternative like AtomicU32 or Mutex.
Convention aside: when you see Arc<T>, assume T must be Send + Sync. When you see Rc<T>, assume T is single-threaded. The community follows this pattern strictly. Mixing Rc with threads is a common source of errors.
Marker traits are promises to the compiler. If you implement them manually, you are signing a contract. Break the contract, and the compiler cannot save you from undefined behavior.
Copy: zero-cost duplication
The Copy trait changes how values are passed and assigned. If a type implements Copy, the compiler performs a bitwise copy instead of a move. The original value remains valid.
Copy is a subtrait of Clone. Every Copy type must also implement Clone. The Clone implementation for Copy types is just a bitwise copy. You can call .clone() on a Copy type, but it does the same thing as assignment.
The compiler forbids Copy on types that implement Drop. If a type has a destructor, copying it would create multiple owners. When one copy drops, it frees resources. The other copy becomes a dangling reference. This rule prevents double-free errors.
Types that own heap memory, like String or Vec, cannot be Copy. They manage dynamic allocation. Copying them would require duplicating the heap data, which is not a bitwise operation. These types implement Clone instead, which performs a deep copy.
/// A point in 2D space.
/// Contains only primitives, so it can be Copy.
#[derive(Copy, Clone)]
struct Point {
x: f64,
y: f64,
}
/// A shape that owns a list of vertices.
/// Vec is not Copy, so Polygon is not Copy.
struct Polygon {
vertices: Vec<Point>,
}
fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
// Copy happens implicitly. p1 is still valid.
let p2 = p1;
println!("p1: {}, {}", p1.x, p1.y);
println!("p2: {}, {}", p2.x, p2.y);
let poly = Polygon { vertices: vec![p1, p2] };
// This would fail. Polygon is not Copy.
// let poly2 = poly;
// println!("{:?}", poly.vertices); // Error: poly moved.
}
Convention aside: always derive Copy and Clone together. The compiler requires Clone for Copy, and writing #[derive(Copy, Clone)] is the standard idiom. It signals to readers that the type is cheap to duplicate.
If your type owns heap memory or holds a lock, it cannot be Copy. The compiler enforces this to prevent double-free errors.
Sized: known size at compile time
The Sized trait means the compiler knows the size of the type at compile time. This is required for stack allocation and passing values by value. Most types are Sized. i32, String, and structs are Sized.
Some types are !Sized (dynamically sized). Slices [T] and trait objects dyn Trait do not have a known size. You cannot put them on the stack directly. You must use a pointer like &[T], Box<[T]>, or &dyn Trait. The pointer is Sized, even though the data it points to is not.
Generic functions require Sized bounds by default. If you write fn process<T>(val: T), the compiler assumes T: Sized. If you try to pass a slice, it fails. You can opt out of the bound using ?Sized.
/// A function that takes a value by value.
/// T must be Sized because the function needs to know
/// how much stack space to reserve for the argument.
fn process_value<T>(val: T) {
println!("Processing value");
}
fn main() {
// This works. i32 is Sized.
process_value(42);
// This fails. [i32] is not Sized.
// let slice: [i32] = [1, 2, 3];
// process_value(slice);
// Use a reference instead. &[i32] is Sized.
let slice = [1, 2, 3];
process_value(&slice);
}
When you see a Sized bound, remember it is just the compiler asking for the size so it can lay out memory. If you do not know the size, use a pointer.
Realistic example: thread-safe configuration
Combine these traits to build a configuration manager that works across threads. The config contains a String, so it is not Copy. It contains only Send + Sync fields, so it is Send + Sync. You can wrap it in Arc and share it.
use std::sync::Arc;
use std::thread;
/// Application configuration.
/// Contains a String, so it is not Copy.
/// Contains only Send/Sync fields, so it is Send and Sync.
struct AppConfig {
database_url: String,
max_retries: u32,
}
/// Worker function that receives a shared reference to config.
/// The compiler checks that AppConfig is Sync, allowing &AppConfig to be sent.
fn worker(config: &AppConfig) {
println!("Connecting to {}", config.database_url);
}
fn main() {
let config = AppConfig {
database_url: String::from("postgres://localhost/db"),
max_retries: 3,
};
// Wrap in Arc to share ownership across threads.
// Arc requires T: Send + Sync. AppConfig satisfies this.
let shared_config = Arc::new(config);
let handles: Vec<_> = (0..3)
.map(|id| {
let config_clone = Arc::clone(&shared_config);
thread::spawn(move || {
// config_clone is moved into the closure.
// The closure takes &AppConfig, which is safe because AppConfig is Sync.
worker(&config_clone);
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
}
The worker function takes &AppConfig. Since AppConfig is Sync, &AppConfig is Send. The thread can take the reference safely. The Arc ensures the config stays alive as long as any thread holds a clone. If you replaced String with Rc<String>, the config would lose Sync, and the Arc would fail. The compiler catches this at the definition site.
Pitfalls and compiler errors
Marker traits cause specific errors when misused. Recognizing them speeds up debugging.
Using Rc in a multi-threaded context triggers E0277 (trait bound not satisfied). Rc is not Send or Sync. The compiler will point to the Rc field and explain the missing trait. Replace Rc with Arc.
Using Cell or RefCell across threads triggers the same error. Cell is not Sync. RefCell is not Sync. Use AtomicU32 for simple counters or Mutex for complex state.
Adding a !Send field to a struct makes the whole struct !Send. If you have a File handle or a raw pointer, the struct cannot be sent to threads. The compiler derives traits recursively. One bad field breaks the whole type.
Implementing Copy on a type with Drop fails with a hard error. The compiler enforces this rule. You cannot derive Copy if you implement Drop. You must remove Drop or drop Copy.
The compiler is your safety net. If it rejects a type for concurrency, it found a race condition or a data race waiting to happen. Fix the type, do not fight the trait.
When to use each trait
Use Send when you need to move ownership of a value from one thread to another. Most types are Send by default unless they contain non-thread-safe handles like Rc or Cell.
Use Sync when you need to share a reference to a value across multiple threads simultaneously. Sync is required for types wrapped in Arc or shared via &T in thread spawners.
Use Copy when your type is a small, stack-only value with no destructor. Derive Copy and Clone for structs containing only primitives, pointers, or other Copy types.
Use Sized when you need to allocate a value on the stack or pass it by value. Generic functions require Sized bounds unless you explicitly opt out with ?Sized.
Reach for unsafe impl Send or unsafe impl Sync only when you are wrapping a foreign type or a raw pointer and you have manually verified the concurrency invariants. This is rare and dangerous.
Reach for ?Sized bounds when writing generic code that must accept slices or trait objects. This is an advanced pattern for library authors.