Skip to content

Q064. What makes a future Send, and why does tokio::spawn require Send + 'static?

Short Answer

A future is Send when all state it may carry across .await can be moved safely between threads. Tokio's multi-thread spawn may resume a task on another worker, so the future and its output must be Send. The 'static bound means the spawned future cannot contain non-'static borrowed data; it does not mean the task lives forever. Use async move with owned values such as Arc, or use a LocalSet for intentionally non-Send tasks.

Deep Dive

Locals that are created and dropped entirely between await points do not become suspended task state and therefore need not be Send. A common failure is keeping Rc, RefCell borrow guards, or non-Send lock guards alive across .await.

'static is required because the caller can drop its local stack frame while the detached task continues. Owning a String, Arc<T>, or other self-contained value satisfies the bound even though that value will be dropped when the task finishes.

tokio::spawn also requires Output: Send + 'static because the result may cross threads through JoinHandle. Current-thread execution with spawn_local relaxes Send, but ownership and cancellation still need explicit design.

Internal Model

The compiler determines the generated future's auto traits from captured values and fields live in suspended states. The executor is free to move the task allocation or schedule its poll on another worker according to its API guarantees.

Bounds are checked at compile time; Tokio does not test task fields dynamically.

Example

rust
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let symbol = Arc::new(String::from("BTC-USD"));
    let task_symbol = Arc::clone(&symbol);

    let task = tokio::spawn(async move {
        tokio::task::yield_now().await;
        task_symbol.len()
    });

    assert_eq!(task.await.unwrap(), symbol.len());
}

Common Mistakes

  • Explaining 'static as "allocated forever" or "must be a static variable."
  • Assuming async move automatically makes every captured value Send.
  • Holding a non-Send value across .await and trying to fix only the spawn call.

Follow-ups

  • How can narrowing a local scope make a future Send?
  • When should LocalSet and spawn_local be used?
  • Why must a spawned task's output also be Send + 'static?

References

  • Async Book
  • Tokio Documentation
  • Rust Reference