How does async_trait::async_trait handle Send bounds for async trait methods?

async_trait::async_trait boxes the returned future from async trait methods as Pin<Box<dyn Future>>, and by default applies a Send bound to make the boxed future Send-safe for use in async runtimes. This allows async trait methods to work with multi-threaded runtimes like tokio, but requires the ?Send opt-out syntax when futures contain non-Send types.

The Core Problem

// Without async_trait, this doesn't work in stable Rust:
 
trait AsyncService {
    async fn process(&self, data: Vec<u8>) -> Vec<u8>;
}
 
// Error: async fn in trait is not stable for all use cases
// The compiler cannot guarantee the future's size at compile time
// because different implementations may have different future sizes

Async trait methods require boxing to have a known size for the vtable.

What async_trait Generates

use async_trait::async_trait;
 
// With the async_trait macro:
 
#[async_trait]
trait AsyncService {
    async fn process(&self, data: Vec<u8>) -> Vec<u8>;
}
 
// The macro transforms the async method into:
 
trait AsyncService {
    fn process<'life0, 'async_trait>(
        &'life0 self,
        data: Vec<u8>,
    ) -> Pin<Box<dyn Future<Output = Vec<u8>> + Send + 'async_trait>>
    where
        Self: Sync + 'async_trait,
        'life0: 'async_trait;
}
 
// The key differences:
// 1. async fn becomes fn returning Pin<Box<dyn Future>>
// 2. The boxed future has + Send bound by default
// 3. Lifetime parameters are added for the async_trait

The macro boxes the future and adds Send bound by default.

Default Send Bound

use async_trait::async_trait;
use std::sync::Arc;
 
#[async_trait]
trait Database {
    async fn query(&self, sql: &str) -> Vec<Row>;
}
 
struct PostgresDb {
    pool: Arc<ConnectionPool>,
}
 
#[async_trait]
impl Database for PostgresDb {
    async fn query(&self, sql: &str) -> Vec<Row> {
        // The generated future is Send because:
        // 1. self is &PostgresDb (Send)
        // 2. sql is &str (Send)
        // 3. Row is Send
        // 4. ConnectionPool is Send
        
        self.pool.execute(sql).await
    }
}
 
// This works with multi-threaded runtimes:
 
async fn use_database(db: Arc<dyn Database>) {
    // Can spawn on multi-threaded runtime because
    // dyn Database::query returns Send future
    tokio::spawn(async move {
        db.query("SELECT * FROM users").await;
    });
}

Default Send bound makes async traits compatible with multi-threaded runtimes.

The Send Bound on Boxed Future

use async_trait::async_trait;
 
#[async_trait]
trait Service {
    async fn handle(&self, request: Request) -> Response;
}
 
// What the macro actually generates for the trait:
 
trait Service {
    fn handle<'life0, 'async_trait>(
        &'life0 self,
        request: Request,
    ) -> Pin<Box<dyn Future<Output = Response> + Send + 'async_trait>>
    where
        Self: Sync + 'async_trait,
        'life0: 'async_trait;
}
 
// Note the: + Send in the boxed future type
// This ensures the returned future can be sent between threads

The Send bound is applied to the boxed future, not the trait itself.

Non-Send Futures with ?Send

use async_trait::async_trait;
use std::rc::Rc;
 
// When a future contains non-Send types, use ?Send
 
#[async_trait]
trait LocalService {
    // ?Send opts out of the Send bound
    async fn process(&self, data: Rc<[u8]>) -> Vec<u8>;
}
 
struct LocalProcessor;
 
#[async_trait]
impl LocalService for LocalProcessor {
    async fn process(&self, data: Rc<[u8]>) -> Vec<u8> {
        // Rc is not Send, so this future cannot be Send
        data.iter().cloned().collect()
    }
}
 
// Without ?Send, this would fail:
// error: `Rc<[u8]>` cannot be sent between threads safely

Use ?Send when the future contains non-Send types like Rc.

The ?Send Syntax Details

use async_trait::async_trait;
 
#[async_trait]
trait MixedService {
    // Default: Send bound applied
    async fn send_method(&self) -> i32;
    
    // Opt out: ?Send removes Send bound
    async fn local_method(&self) -> i32;
}
 
// The ?Send in the return type signals to async_trait
// that this method's future does not need to be Send
 
// Generated code for local_method:
 
trait MixedService {
    // send_method gets Send bound
    fn send_method<'life0, 'async_trait>(
        &'life0 self,
    ) -> Pin<Box<dyn Future<Output = i32> + Send + 'async_trait>>
    where
        Self: Sync + 'async_trait,
        'life0: 'async_trait;
    
    // local_method without Send bound
    fn local_method<'life0, 'async_trait>(
        &'life0 self,
    ) -> Pin<Box<dyn Future<Output = i32> + 'async_trait>>  // No + Send
    where
        Self: 'async_trait,
        'life0: 'async_trait;
}

Methods with ?Send in return type get boxed futures without the Send bound.

Common Non-Send Types

use async_trait::async_trait;
use std::rc::Rc;
use std::cell::RefCell;
 
// Types that are NOT Send:
 
struct NotSendTypes {
    rc: Rc<String>,           // Rc is !Send
    ref_cell: RefCell<i32>,   // RefCell is !Send
    raw_pointer: *const u8,   // Raw pointers are !Send (by default)
}
 
#[async_trait]
trait Example {
    // This REQUIRES ?Send because Rc is used:
    async fn use_rc(&self, data: Rc<Vec<u8>>) -> usize {
        data.len()
    }
    
    // This also requires ?Send because RefCell is used:
    async fn use_refcell(&self, cell: RefCell<i32>) -> i32 {
        *cell.borrow()
    }
}
 
// Without ?Send, you get errors like:
// `Rc<Vec<u8>>` cannot be sent between threads safely
// the trait `Send` is not implemented for `Rc<Vec<u8>>`

Any non-Send type captured by the future requires ?Send.

Self Types and Send Bounds

use async_trait::async_trait;
 
// The Self type must also be Send-compatible
 
#[async_trait]
trait Handler {
    async fn handle(&self);
}
 
// For Send methods, Self must be Send-safe:
 
struct SendHandler {
    data: Vec<u8>,  // Send
}
 
#[async_trait]
impl Handler for SendHandler {
    async fn handle(&self) {
        // Works: SendHandler is Send
    }
}
 
// Non-Send self types require ?Send:
 
struct NotSendHandler {
    data: Rc<Vec<u8>>,  // !Send
}
 
#[async_trait]
impl Handler for NotSendHandler {
    async fn handle(&self) {
        // Without ?Send on the trait method:
        // error: `NotSendHandler` cannot be sent between threads safely
    }
}
 
// Alternative: define trait with ?Send methods for non-Send types
 
#[async_trait]
trait LocalHandler {
    async fn handle(&self);  // No Send requirement
}

The implementing type must be Send if the trait method requires Send.

Sync Requirements

use async_trait::async_trait;
 
// For Send methods, the trait also requires Self: Sync
 
#[async_trait]
trait Service {
    async fn serve(&self);
}
 
// Generated code includes:
// where Self: Sync + 'async_trait
 
// This is because &self is borrowed across await points
// The reference must be valid across threads
 
struct NonSyncService {
    cell: std::cell::Cell<i32>,  // Cell is !Sync
}
 
#[async_trait]
impl Service for NonSyncService {
    async fn serve(&self) {
        // Error: `NonSyncService` cannot be shared between threads safely
        // the trait `Sync` is not implemented
    }
}
 
// Solutions:
// 1. Use ?Send to remove Sync requirement
// 2. Use Sync types (AtomicI32 instead of Cell)
// 3. Use Mutex for interior mutability

Send methods also require Self: Sync because &self lives across await points.

Interior Mutability Patterns

use async_trait::async_trait;
use std::sync::Mutex;
use std::cell::RefCell;
 
#[async_trait]
trait Counter {
    async fn increment(&self) -> i32;
}
 
// Send-compatible: use Mutex (Sync + Send)
 
struct SendCounter {
    count: Mutex<i32>,
}
 
#[async_trait]
impl Counter for SendCounter {
    async fn increment(&self) -> i32 {
        let mut guard = self.count.lock().unwrap();
        *guard += 1;
        *guard
    }
}
 
// Non-Send: use RefCell (Sync but not Send-safe across threads)
 
struct LocalCounter {
    count: RefCell<i32>,
}
 
// This REQUIRES ?Send because RefCell is in the future
 
#[async_trait]
impl Counter for LocalCounter {
    async fn increment(&self) -> i32 {
        // Error without ?Send: RefCell is not Send-safe
        let mut count = self.count.borrow_mut();
        *count += 1;
        *count
    }
}

Use Mutex for Send-compatible interior mutability; RefCell requires ?Send.

Runtime Compatibility

use async_trait::async_trait;
use tokio::task::spawn;
 
#[async_trait]
trait Task {
    async fn run(&self);
}
 
async fn run_on_runtime<T: Task + Send + Sync>(task: T) {
    // tokio::spawn requires Send future
    spawn(async move {
        task.run().await;
    });
}
 
// This works because:
// - T: Send + Sync (the implementing type)
// - Task::run returns Send future (default)
// - The spawned future is Send
 
// With ?Send:
 
#[async_trait]
trait LocalTask {
    async fn run(&self);  // No Send bound
}
 
async fn run_local<T: LocalTask>(task: T) {
    // Cannot use tokio::spawn - the future is not Send
    // spawn_local(task.run());  // Would need local runtime
    
    // Must run in place:
    task.run().await;
}

Send futures work with tokio::spawn; ?Send futures require local executors.

Boxed Future Size Impact

use async_trait::async_trait;
 
// Boxing has performance implications:
 
trait Service {
    async fn process(&self, data: Vec<u8>) -> Vec<u8>;
}
 
// Generated code boxes the entire future:
 
impl Service for MyService {
    fn process<'life0, 'async_trait>(
        &'life0 self,
        data: Vec<u8>,
    ) -> Pin<Box<dyn Future<Output = Vec<u8>> + Send + 'async_trait>>
    where
        Self: Sync + 'async_trait,
        'life0: 'async_trait,
    {
        Box::pin(async move {
            // Entire async block is boxed
            // Allocation on every call
            self.do_process(data).await
        })
    }
}
 
// Performance impact:
// 1. Heap allocation per call
// 2. Dynamic dispatch for poll()
// 3. Extra indirection
// 4. But: stable Rust compatibility
 
// Alternative (when stable): return impl Future
// trait Service {
//     fn process(&self, data: Vec<u8>) -> impl Future<Output = Vec<u8>>;
// }
// This doesn't work for trait objects (dyn Service)

Boxing enables trait object compatibility at the cost of allocation.

Generic Send Bounds

use async_trait::async_trait;
 
#[async_trait]
trait Repository<T> {
    async fn get(&self, id: u64) -> Option<T>;
}
 
// Generic implementations:
 
struct Cache<T> {
    data: Vec<T>,
}
 
#[async_trait]
impl<T: Send + Sync + 'static> Repository<T> for Cache<T> {
    async fn get(&self, id: u64) -> Option<T> {
        // T must be Send + Sync for the future to be Send
        self.data.get(id as usize).cloned()
    }
}
 
// If T is not Send:
 
struct NotSendType {
    rc: std::rc::Rc<()>,
}
 
// This won't work:
// impl Repository<NotSendType> for Cache<NotSendType> { ... }
// Error: NotSendType does not implement Send
 
// Solution: use ?Send on the trait

Generic types must be Send for default async trait methods.

Conditional Send Bounds

use async_trait::async_trait;
 
// Sometimes you want Send conditionally
 
#[async_trait]
trait ConditionalService {
    // This doesn't work directly - async_trait doesn't support
    // conditional Send bounds on individual methods
    
    // Alternative: define two traits
    
    // For Send futures (multi-threaded)
    async fn send_process(&self);
    
    // For local futures (single-threaded)
    async fn local_process(&self);
}
 
// Or use generic parameters:
 
#[async_trait]
trait GenericService<S: Send> {
    async fn process(&self, data: S) -> S;
}

Complex Send conditions may require separate traits or redesign.

Comparison Table

use async_trait::async_trait;
 
fn comparison_table() {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ Aspect               β”‚ Send Method            β”‚ ?Send Method          β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ Future bound         β”‚ Pin<Box<dyn Future + Send>> β”‚ Pin<Box<dyn Future>>β”‚
    // β”‚ Self requirement     β”‚ Self: Sync             β”‚ No Sync requirement   β”‚
    // β”‚ tokio::spawn         β”‚ Compatible            β”‚ Not compatible        β”‚
    // β”‚ Runtime              β”‚ Multi-threaded        β”‚ Single-threaded       β”‚
    // β”‚ Rc/RefCell           β”‚ Not allowed           β”‚ Allowed              β”‚
    // β”‚ Cross-thread safety  β”‚ Required              β”‚ Not required          β”‚
    // β”‚ Performance          β”‚ Same (boxed)          β”‚ Same (boxed)          β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
}
 
// Default behavior: Send (multi-threaded compatible)
// Use ?Send when: Rc, RefCell, or other !Send types are needed

Send methods are the default for multi-threaded compatibility.

Complete Summary

use async_trait::async_trait;
 
#[async_trait]
trait CompleteSummary {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ What async_trait does:                                                  β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ 1. Transforms async fn into fn returning Pin<Box<dyn Future>>         β”‚
    // β”‚ 2. Adds Send bound to boxed future by default                          β”‚
    // β”‚ 3. Requires Self: Sync for Send methods (reference across await)      β”‚
    // β”‚ 4. Supports ?Send opt-out for non-Send futures                         β”‚
    // β”‚ 5. Enables trait objects with async methods                            β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
}
 
// Key behaviors:
//
// Default (Send bound):
// - async fn method() -> T;
// - Generates: Pin<Box<dyn Future<Output = T> + Send>>
// - Self must be Sync
// - Works with tokio::spawn
// - Multi-threaded runtime compatible
//
// With ?Send:
// - async fn method() -> T;
// - Generates: Pin<Box<dyn Future<Output = T>>>  (no Send)
// - No Sync requirement
// - Cannot use tokio::spawn
// - Single-threaded or local runtime only
//
// Use ?Send when:
// - Capturing Rc<T>
// - Capturing RefCell<T>
// - Capturing *const T or *mut T
// - Self type is not Sync
// - Any captured type is !Send
//
// The Send bound ensures futures work with multi-threaded runtimes
// but restricts what types can be used in the async block.

Key insight: async_trait::async_trait boxes async method futures with a Send bound by default, making them compatible with multi-threaded runtimes like tokio. The Send bound requires that all captured types are Send and Self is Sync. When using non-Send types like Rc or RefCell, use ?Send in the return type to opt out of the Send requirementβ€”but then the future cannot be spawned on multi-threaded runtimes. The trade-off is between multi-threaded compatibility (default Send) and single-threaded flexibility (?Send).