Rust Patterns That Matter #13: 'static + Clone - The Escape Hatch
Post 13 of 22 in Rust Patterns That Matter. Companion series: Building a Chat Server in Rust.
Previous: #12: Custom Iterators | Next: #14: Enum Dispatch
You're spawning a thread or an async task. The API requires 'static.
Your data borrows from something. The compiler won't let it through. You try adding
lifetime annotations, restructuring, wrapping things in Arc. The code
gets more complex with each attempt. There's a simpler answer: clone the data.
The motivation
fn process(data: &str) {
std::thread::spawn(|| {
println!("{}", data);
});
}
error[E0373]: closure may outlive the current function,
but it borrows `data`, which is owned by the current function
thread::spawn requires the closure to be 'static -
it can't borrow from the enclosing function because the thread might outlive it.
The same applies to tokio::spawn, rayon::spawn, and any
API that takes F: FnOnce() + Send + 'static.
Why everything wants 'static
A spawned task or thread has no guaranteed lifetime. It might run for a microsecond
or for the rest of the program. The runtime can't prove that borrowed data will still
be valid when the task reads it. 'static means "this value is
self-contained - it owns everything it needs." No dangling references possible.
This applies to any owned type: String, Vec<u8>,
i32, PathBuf. All owned types are 'static
because they don't borrow from anything. The bound isn't asking for immortal data
- it's asking for data that doesn't depend on someone else's lifetime.
The pattern: clone and move
fn process(data: &str) {
let data = data.to_string(); // owned copy
std::thread::spawn(move || {
println!("{}", data);
});
}
data.to_string() creates an owned String. The
move closure takes ownership of it. The closure is now
'static because it owns everything it captures. No lifetime
issues.
For structs with multiple fields:
fn spawn_worker(config: &Config) {
let config = config.clone();
tokio::spawn(async move {
// config is owned — no lifetime constraints
do_work(&config).await;
});
}
"But cloning is wasteful!"
This is the most common objection, and it's almost always premature. Consider what a clone actually costs:
- Cloning a
String: one heap allocation +memcpyof the string's bytes - Cloning a
Vec<T>: one heap allocation +memcpy(forCopytypes) or element-wise cloning - Cloning a small struct with a few
Stringfields: a handful of allocations
Now consider the context. If you're spawning a task to make a network request (milliseconds), query a database (milliseconds), or do any kind of I/O, the clone is nanoseconds of noise. You can't measure it. The anxiety about cloning is almost always misplaced.
Profile before you optimise. If your profiler shows that cloning is a bottleneck, address it. Until then, clone freely.
Arc as a middle ground
When the data is genuinely large (megabytes, or cloned thousands of times per second), cloning might show up in profiles. In that case, share instead of copying:
use std::sync::Arc;
fn spawn_workers(data: Arc<LargeDataset>) {
for _ in 0..10 {
let data = Arc::clone(&data);
tokio::spawn(async move {
process(&data).await;
});
}
}
Arc::clone is a reference count increment - a single atomic
operation. All ten tasks share the same LargeDataset allocation.
Arc<T> is 'static + Send (when
T: Send + Sync), so it satisfies spawn bounds.
The decision tree
- Does the task need
'static? If the data is already owned, you're done - justmoveit in. - Is the data small or the clone infrequent? Clone it. This is the right answer 90% of the time.
- Is the data large and shared by many tasks? Use
Arc. - Does the data need to be mutated by multiple tasks? Use
Arc<Mutex<T>>(#19).
When to use it
- Spawning threads or async tasks that need data from the calling scope
- Callbacks registered across different lifetimes
- Any time the compiler demands
'staticand your data borrows
The instinct to avoid cloning comes from languages where copies are expensive by default. In Rust, cloning is explicit, visible, and usually cheap. When it's not, the profiler will tell you.
See it in practice: Building a Chat Server #3: Parsing and Performance uses this pattern for moving parsed data across scope boundaries.
Telex