Rust Error Future Cannot Be Sent Between Threads Safely
Tokio spawn needs a Send future. When the compiler says the future cannot be sent between threads safely, it points at the value held across an await.

"Future cannot be sent between threads safely" is the longest error message most Rust programmers see, and the length hides a short diagnosis. A future passed to tokio::spawn must be Send, a future is Send only if everything it holds across an .await is Send, and somewhere you are holding something that is not. The message names it. This post shows how to read the error from the bottom up, then goes through the six values that cause it in practice (Rc, a RefCell borrow, a std mutex guard, a trait object without Send, a generic type parameter, a raw pointer), with the failing code, the compiler's message and the repair for each, and a helper that locates the culprit in a large future. Everything compiled on Rust 1.99, edition 2024, with tokio 1.
How to read it
The message is a chain of notes, and the order is the reverse of how you want to read it. Skip the top: it says the future "is not Send" and shows the whole async block. Find the line that begins note: future is not Send as this value is used across an await, which names a variable and the type that is not Send, and shows two code locations: where the value was created and the .await that comes while it is still alive. The last note, "required by a bound in tokio::spawn", is why the compiler cares at all. Two facts to hold: only values alive across an await matter (a non-Send value created and dropped between awaits is fine), and the check is by scope, not by use, as shown in mutex guards across await.
Culprit 1: Rc
use std::rc::Rc;
#[tokio::main]
async fn main() {
let shared = Rc::new(String::from("config"));
let task = tokio::spawn(async move {
tokio::task::yield_now().await;
println!("{}", shared);
});
task.await.unwrap();
}error: future cannot be sent between threads safely
--> src/main.rs:7:16
|
7 | let task = tokio::spawn(async move {
| ________________^
8 | | tokio::task::yield_now().await;
9 | | println!("{}", shared);
10 | | });
| |______^ future created by async block is not `Send`
|
= help: within `{async block@src/main.rs:7:29: 7:39}`, the trait `Send` is not implemented for `Rc<String>`
note: captured value is not `Send`
--> src/main.rs:9:24
|
9 | println!("{}", shared);
| ^^^^^^ has type `Rc<String>` which is not `Send`
note: required by a bound in `tokio::spawn`
--> tokio-1.54.1/src/task/spawn.rs:176:21
|
174 | pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
| ----- required by a bound in this function
175 | where
176 | F: Future + Send + 'static,
| ^^^^ required by this bound in `spawn`Rc has a non-atomic reference count; two threads touching it could corrupt the count. The fix is the thread-safe sibling, Arc, which pays an atomic increment per clone. If the code never leaves one thread, use a LocalSet instead (see LocalSet). The comparison is in Box, Rc and Arc.
Culprit 2: a RefCell borrow
A RefCell itself can be sent (it is Send if its contents are), so a task that owns one is fine. A borrow guard, Ref or RefMut, held across an await is not Send, and it is also a run-time hazard: another task that borrows the cell while you sleep panics with "already borrowed".
use std::cell::RefCell;
#[tokio::main]
async fn main() {
let cell = RefCell::new(vec![1, 2, 3]);
let task = tokio::spawn(async move {
let items = cell.borrow();
tokio::task::yield_now().await;
println!("{}", items.len());
});
task.await.unwrap();
}error: future cannot be sent between threads safely
--> src/main.rs:7:16
|
7 | let task = tokio::spawn(async move {
| ________________^
8 | | let items = cell.borrow();
9 | | tokio::task::yield_now().await;
10 | | println!("{}", items.len());
11 | | });
| |______^ future created by async block is not `Send`
|
= help: within `{async block@src/main.rs:7:29: 7:39}`, the trait `Send` is not implemented for `NonNull<Vec<i32>>`
note: future is not `Send` as this value is used across an await
--> src/main.rs:9:34
|
8 | let items = cell.borrow();
| ----- has type `std::cell::Ref<'_, Vec<i32>>` which is not `Send`
9 | tokio::task::yield_now().await;
| ^^^^^ await occurs here, with `items` maybe used later
note: required by a bound in `tokio::spawn`
--> tokio-1.54.1/src/task/spawn.rs:176:21
|
174 | pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
| ----- required by a bound in this function
175 | where
176 | F: Future + Send + 'static,
| ^^^^ required by this bound in `spawn`Copy out what you need before the await (let n = cell.borrow().len();) or end the borrow's scope with braces. Clippy's await_holding_refcell_ref warns about this even when no spawn forces the question.
Culprit 3: a std mutex guard
Covered at length in mutex guards across await: the guard of std::sync::Mutex is not Send. End its scope before the await, or switch to tokio::sync::Mutex when the lock has to span the wait.
Culprit 4: a trait object that did not promise Send
This one comes from your own types. A Box<dyn Handler> holds some unknown type, and without + Send in the type, the compiler must assume the worst:
trait Handler {
fn handle(&self, input: &str) -> String;
}
struct Upper;
impl Handler for Upper {
fn handle(&self, input: &str) -> String {
input.to_uppercase()
}
}
#[tokio::main]
async fn main() {
let handler: Box<dyn Handler> = Box::new(Upper);
let task = tokio::spawn(async move {
tokio::task::yield_now().await;
handler.handle("hello")
});
println!("{}", task.await.unwrap());
}error: future cannot be sent between threads safely
--> src/main.rs:17:16
|
17 | let task = tokio::spawn(async move {
| ________________^
18 | | tokio::task::yield_now().await;
19 | | handler.handle("hello")
20 | | });
| |______^ future created by async block is not `Send`
|
= help: the trait `Send` is not implemented for `dyn Handler`
note: captured value is not `Send`
--> src/main.rs:19:9
|
19 | handler.handle("hello")
| ^^^^^^^ has type `Box<dyn Handler>` which is not `Send`
note: required by a bound in `tokio::spawn`
--> tokio-1.54.1/src/task/spawn.rs:176:21
|
174 | pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
| ----- required by a bound in this function
175 | where
176 | F: Future + Send + 'static,
| ^^^^ required by this bound in `spawn`The message says dyn Handler cannot be sent. Upper is perfectly Send, but the type Box<dyn Handler> erased that fact. Add it back to the object type, Box<dyn Handler + Send>, or better, to the trait itself with trait Handler: Send + Sync, so that every implementor must be thread-safe and every object type inherits the bound. The second form is almost always right for a trait that will be stored and spawned. The design question about when to put these bounds on a trait is covered in Send and Sync.
Culprit 5: a generic type that is not known to be Send
In a generic function, the compiler knows nothing about T unless you tell it:
async fn run_in_background<T: std::fmt::Debug + 'static>(value: T) {
let task = tokio::spawn(async move {
tokio::task::yield_now().await;
println!("{value:?}");
});
task.await.unwrap();
}
#[tokio::main]
async fn main() {
run_in_background(42).await;
}error: future cannot be sent between threads safely
--> src/main.rs:2:16
|
2 | let task = tokio::spawn(async move {
| ________________^
3 | | tokio::task::yield_now().await;
4 | | println!("{value:?}");
5 | | });
| |______^ future created by async block is not `Send`
|
note: captured value is not `Send`
--> src/main.rs:4:20
|
4 | println!("{value:?}");
| ^^^^^ has type `T` which is not `Send`
note: required by a bound in `tokio::spawn`
--> tokio-1.54.1/src/task/spawn.rs:176:21
|
174 | pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
| ----- required by a bound in this function
175 | where
176 | F: Future + Send + 'static,
| ^^^^ required by this bound in `spawn`
help: consider further restricting type parameter `T` with trait `Send`
|
1 | async fn run_in_background<T: std::fmt::Debug + 'static + std::marker::Send>(value: T) {
| +++++++++++++++++++The call with an i32 would be fine; the generic body is checked once, for all possible T. The repair is a bound in the signature: T: Debug + Send + 'static. This is the most common source of the error in library code, and it is also the one where adding the bound is a visible API change. Futures returned from async trait methods hit the same issue, discussed in async fn in trait errors.
Culprit 6: a raw pointer or an FFI handle
Raw pointers are never Send. A struct wrapping a C library handle inherits that. If the library documents the handle as safe to use from any one thread at a time, wrap it and assert it yourself with unsafe impl Send, writing the invariant in a comment as in Send and Sync. Otherwise keep the handle on one thread and talk to it through a channel.
Finding the culprit in a large future
In a function with a few hundred lines, the error's first notes may list a type you do not recognise, such as a closure or a nested async block. A one-line probe pinpoints the value. Wrap suspect expressions in a function that demands Send:
use std::rc::Rc;
fn require_send<T: Send>(_: &T) {}
#[tokio::main]
async fn main() {
let name = String::from("ok");
let pointer = Rc::new(1u8);
require_send(&name);
require_send(&pointer);
println!("{name} {pointer}");
}error[E0277]: `Rc<u8>` cannot be sent between threads safely
--> src/main.rs:11:18
|
11 | require_send(&pointer);
| ------------ ^^^^^^^^ `Rc<u8>` cannot be sent between threads safely
| |
| required by a bound introduced by this call
|
= help: the trait `Send` is not implemented for `Rc<u8>`
note: required by a bound in `require_send`
--> src/main.rs:3:20
|
3 | fn require_send<T: Send>(_: &T) {}
| ^^^^ required by this bound in `require_send`Move the probe line to where each suspect is alive; the first one the compiler rejects is the culprit. For the whole future, fn assert_send<F: Future + Send>(_: F) {} called with the future (before spawning it) gives the same message in a smaller context, and a unit test containing that call keeps the guarantee from regressing, the technique used for public types in Send and Sync.
When the fix is not to make it Send
If the non-Send value is essential (a GUI handle, a thread-local cache, an Rc graph), do not contort the code; change where it runs. A LocalSet or a current-thread runtime accepts non-Send futures, at the cost of running them on one thread. Or restructure so the non-Send value is created after the last await in the task and dropped before the next one, which is often a two-line change. Moving the non-Send work to a dedicated thread with a channel to the async side is the third option, and the right one for FFI. The related fix for the mirror-image error, calling blocking code from async, is in blocking the Tokio runtime.


