The Sync Bound Nobody Asked For
- title
- The Sync Bound Nobody Asked For
- type
- summary
- summary
- verrchu's deep-dive on why
&selfin async trait methods returningSendfutures silently demandsSelf: Sync, and why&mut selfquietly fixes it - parent
- rust-send-sync
- tags
- rust, async, type-system
- sources
- verrchu-sync-bound
- created
- 2026-05-10
- updated
- 2026-05-10
verrchu's first blog post tracks a Rust async trait error that looks like a Send problem but is really a Sync problem in disguise. The four-cell truth table is the framing:
impl Send + Sync |
impl Send + !Sync |
|
|---|---|---|
future without + Send bound |
compiles | compiles |
future with + Send bound |
compiles | fails |
The failing case (case 2.2) only fails because of two subtle Rust rules:
tokio::spawnrequiresF: Send + 'static, so the returned future has to beSend&T: SendrequiresT: Sync(whereas&mut T: Sendonly requiresT: Send)
So when the trait is fn run(&self) -> impl Future<Output = ()> + Send, the &self capture inside the future implies the future captures a &Self reference. For that capture to be Send, Self has to be Sync. The Sync requirement was hiding inside &self the whole time โ a foreign !Sync field (Cell<()>) just makes it visible.
Two fixes:
- Wrap the
!Syncfield in aMutexโ works becauseMutex<T>: Synconly requiresT: Send, but adds runtime locking overhead even thoughSelfis never actually shared between threads - Switch
&selfto&mut selfโ&mut T: Sendonly requiresT: Send, so theSyncdemand disappears entirely
The second fix is counter-intuitive. The instinct in Rust is to reach for & because it's "tighter" (no mutation). Here it's looser in the trait-bound sense. The real meaning of &mut T is unique reference, not mutable reference โ uniqueness is what rules out cross-thread sharing, which is what removes the Sync requirement. (Niko Matsakis's "Focusing on ownership" is the canonical write-up of &mut as uniqueness.)
The post is short, well-illustrated with full compile errors, and a good entry point for the broader topic of how Send/Sync cascade through async-trait return types. Practical takeaway: when an async trait method must return a Send future, default to &mut self unless you specifically need shared access.
Cross-references: rust-send-sync, verrchu-blog, message-passing-vs-shared-memory.