Cached at:
05/08/26, 04:30 PM
# The `Sync` bound nobody asked for
Source: [https://verrchu.github.io/blog/1-the-sync-bound-nobody-asked-for/](https://verrchu.github.io/blog/1-the-sync-bound-nobody-asked-for/)
May 4, 2026
`&self`on an async trait method whose returned future must be`Send`implicitly forces`Sync`on the impl type — even if neither the trait nor its callers ever ask for`Sync`\.
Most async runtimes spawn futures onto a thread pool, which means a spawned future has to be safe to move between threads\.[`tokio::spawn`](https://docs.rs/tokio/1.52.2/tokio/task/fn.spawn.html)makes the requirement explicit:
```
pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
```
`F: Send`cascades through everything the future captures\.
Whenever a future captures a reference and itself has to be`Send`, two facts about Rust’s reference types matter:
- `&T: Send`requires`T: Sync`\.
- `&mut T: Send`only requires`T: Send`\.
So a`Send`future that captures`&mut T`only needs`T: Send`, but a`Send`future that captures`&T`needs`T: Sync`\.
In the example below, everything compiles because`MyWorker`is trivially`Send \+ Sync`\.
```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
```
```
pub trait Worker {
fn work(&self) -> impl Future<Output = ()> + Send;
}
#[allow(dead_code)]
struct MyWorker;
static_assertions::assert_impl_all!(MyWorker: Send);
static_assertions::assert_impl_all!(MyWorker: Sync);
impl Worker for MyWorker {
async fn work(&self) {}
}
pub fn spawn<W: Worker + Send + 'static>(w: W) {
tokio::spawn(async move {
loop {
w.work().await;
}
});
}
```
The`Worker`trait only visibly asks for`Send`, so giving the impl type interior mutability seems reasonable\. But`Cell`is`Send`and`\!Sync`, so it makes`MyWorker``\!Sync`too, which breaks the`Sync`requirement coming from`&self`\.
```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
```
```
use std::cell::Cell;
pub trait Worker {
fn work(&self) -> impl Future<Output = ()> + Send;
}
#[allow(dead_code)]
struct MyWorker(Cell<()>);
static_assertions::assert_impl_all!(MyWorker: Send);
static_assertions::assert_not_impl_any!(MyWorker: Sync);
impl Worker for MyWorker {
async fn work(&self) {}
}
pub fn spawn<W: Worker + Send + 'static>(w: W) {
tokio::spawn(async move {
loop {
w.work().await;
}
});
}
```
```
error: future cannot be sent between threads safely
--> examples/step-2.rs:14:25
|
14 | async fn work(&self) {}
| ^ future returned by `work` is not `Send`
|
= help: within `MyWorker`, the trait `Sync` is not implemented for `Cell<()>`
= note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock`
note: captured value is not `Send` because `&` references cannot be sent unless their referent is `Sync`
--> examples/step-2.rs:14:19
|
14 | async fn work(&self) {}
| ^^^^^ has type `&MyWorker` which is not `Send`, because `MyWorker` is not `Sync`
note: required by a bound in `Worker::work::{anon_assoc#0}`
--> examples/step-2.rs:4:50
|
4 | fn work(&self) -> impl Future<Output = ()> + Send;
| ^^^^ required by this bound in `Worker::work::{anon_assoc#0}`
```
The error walks the chain:`fn work`captures`&self`as`&MyWorker`; for the returned future to satisfy`\+ Send`,`&MyWorker`has to be`Send`; and`&T: Send`only holds when`T: Sync`\. The`&self`parameter has been demanding`Sync`on`Self`all along —`Cell`just made the demand visible\.
The cheap fix is to make`Self: Sync`: swap the non\-`Sync`interior\-mutability primitive for a`Sync`one \(a`Mutex`, an`RwLock`, an atomic\)\. The impl type becomes`Sync`and the trait compiles unchanged\. But we’ve added synchronisation overhead on every state access for a worker whose state is only ever touched from inside a single spawned task\. Suboptimal™\.
The better move is to switch`&self`to`&mut self`\.`&mut T: Send`requires only`T: Send`, no`Sync`involved:
```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
```
```
use std::cell::Cell;
pub trait Worker {
fn work(&mut self) -> impl Future<Output = ()> + Send;
}
#[allow(dead_code)]
struct MyWorker(Cell<()>);
static_assertions::assert_impl_all!(MyWorker: Send);
static_assertions::assert_not_impl_any!(MyWorker: Sync);
impl Worker for MyWorker {
async fn work(&mut self) {}
}
pub fn spawn<W: Worker + Send + 'static>(mut w: W) {
tokio::spawn(async move {
loop {
w.work().await;
}
});
}
```
This compiles\. Now the trait carries no`Sync`requirement anywhere\.
Underneath all of this is`&mut T`being the unique reference, not the mutable one\. The instinct in Rust is to reach for`&`over`&mut`to tighten the contract: no mutation allowed\. Here it goes the other way\.`&mut self`guarantees unique access to`Self`for the duration of the call, which rules out cross\-thread sharing and drops the`Sync`bound with it\.
## Links
- Niko Matsakis,[*Focusing on ownership*](https://smallcultfollowing.com/babysteps/blog/2014/05/13/focusing-on-ownership/)— the canonical writeup on`&mut`as uniqueness rather than mutation\.
---
*Full accompanying source code can be found[here](https://github.com/verrchu/blog/tree/main/content/1-the-sync-bound-nobody-asked-for)\. Built with`rustc 1\.95\.0`\. Library versions used:`tokio`1\.52\.2\.*