Map
โ†‘Rust Send and Sync

The Sync Bound Nobody Asked For

Wiki summaryrustasynctype-system โ†ณ show in map Markdown
title
The Sync Bound Nobody Asked For
type
summary
summary
verrchu's deep-dive on why &self in async trait methods returning Send futures silently demands Self: Sync, and why &mut self quietly fixes it
tags
rust, async, type-system
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::spawn requires F: Send + 'static, so the returned future has to be Send
  • &T: Send requires T: Sync (whereas &mut T: Send only requires T: 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:

  1. Wrap the !Sync field in a Mutex โ€” works because Mutex<T>: Sync only requires T: Send, but adds runtime locking overhead even though Self is never actually shared between threads
  2. Switch &self to &mut self โ€” &mut T: Send only requires T: Send, so the Sync demand 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.