How to Fix: `T cannot be sent between threads safely` in `component::bindgen!` output.

7 min read

Fixing T cannot be sent between threads safely in component::bindgen! output

If wasmtime::component::bindgen! generates code that suddenly fails with T cannot be sent between threads safely, the problem is usually not your WIT world itself. The failure typically comes from a mismatch between the generated bindings, the host traits they require, and the thread-safety guarantees imposed by Wasmtime when resources cross async or multi-threaded boundaries.

Given a macro like this:

bindgen!({
    world: "gav",
    with: {
        "wasi": wasmtime_wasi::bindings,
        "wasi:http": wasmtime_wasi_http::bindings::http,
    },
});

the generated code often composes your world bindings with WASI and WASI HTTP bindings. Those host-side interfaces frequently expect the store data and related state to satisfy Send, especially when async execution is enabled or when Wasmtime may move futures across threads.

Understanding the Root Cause

This error happens because Rust refuses to move a type between threads unless it implements the Send auto trait. In the context of component::bindgen!, the generated bindings may produce trait bounds that effectively require your host state, resource table, closure, or captured dependency to be thread-safe.

In practice, one of these is usually true:

  • Your store data contains a non-Send type such as Rc<T>, RefCell<T>, or another single-threaded object.
  • Your implementation of the generated host traits captures non-Send state.
  • You are combining your world with wasmtime_wasi or wasmtime_wasi_http, and those bindings introduce stricter thread-safety requirements than your custom world alone.
  • Async support causes futures to require Send, which propagates into the generated API surface.

The important detail is that this is usually a trait-bound propagation issue. The macro output is not randomly broken; it is reflecting the requirements of the imported interfaces and the runtime model. Once one part of the host integration needs Send, that requirement bubbles up into the generated types and trait impls.

Common non-Send offenders include:

Rc<T>
RefCell<T>
*mut T
std::cell::Cell<T>
std::sync::MutexGuard<'_, T>
certain library handles that are intentionally single-threaded

By contrast, these patterns are usually safe in this scenario:

Arc<T>
Mutex<T>
RwLock<T>
Arc<Mutex<T>>
Arc<RwLock<T>>

Step-by-Step Solution

The fix is to make sure the data flowing through the generated bindings is Send wherever required.

1. Inspect your store data type

If you have something like this:

use std::rc::Rc;
use std::cell::RefCell;

struct MyState {
    cache: Rc<RefCell<MyCache>>,
    ctx: wasmtime_wasi::WasiCtx,
    table: wasmtime_wasi::ResourceTable,
}

that is a red flag. Rc<RefCell<...>> is not thread-safe.

Replace it with thread-safe primitives:

use std::sync::{Arc, Mutex};

struct MyState {
    cache: Arc<Mutex<MyCache>>,
    ctx: wasmtime_wasi::WasiCtx,
    table: wasmtime_wasi::ResourceTable,
}

2. Confirm your state implements the required WASI view traits

When using with mappings for WASI and WASI HTTP, your state usually needs to expose the correct views:

use wasmtime_wasi::{ResourceTable, WasiCtx, WasiView};

struct MyState {
    table: ResourceTable,
    ctx: WasiCtx,
}

impl WasiView for MyState {
    fn table(&mut self) -> &mut ResourceTable {
        &mut self.table
    }

    fn ctx(&mut self) -> &mut WasiCtx {
        &mut self.ctx
    }
}

If your state contains extra fields, ensure those fields are also Send when required.

3. Replace single-threaded shared ownership types

If your code uses these patterns anywhere in host bindings or state:

Rc<T>
RefCell<T>
Rc<RefCell<T>>

migrate them to:

Arc<T>
Mutex<T>
RwLock<T>
Arc<Mutex<T>>

Example migration:

use std::rc::Rc;
use std::cell::RefCell;

struct OldState {
    sessions: Rc<RefCell<Vec<String>>>,
}
use std::sync::{Arc, Mutex};

struct NewState {
    sessions: Arc<Mutex<Vec<String>>>,
}

4. Check async host functions and captured values

If you implement host functions that return async futures, anything captured by those futures may also need to be Send.

let shared = std::sync::Arc::new(std::sync::Mutex::new(Service::new()));

// Good: Arc<Mutex<...>> is appropriate for cross-thread async use.

Be careful with references or guards that cannot outlive the current thread context.

5. Keep generated bindings and Wasmtime crates version-aligned

A subtle source of confusing trait errors is a version mismatch between:

  • wasmtime
  • wasmtime-wasi
  • wasmtime-wasi-http

Use matching crate versions in Cargo.toml:

[dependencies]
wasmtime = "MATCHING_VERSION"
wasmtime-wasi = "MATCHING_VERSION"
wasmtime-wasi-http = "MATCHING_VERSION"

If one crate is newer or older, generated trait expectations may no longer line up cleanly.

6. Rebuild and inspect the concrete type named in the compiler error

The Rust compiler usually tells you exactly which type is not Send. Read the full error chain carefully. It often points to a deeply nested field such as:

Rc<RefCell<...>>
*const ...
Box<dyn Trait>
Box<dyn Trait + Send> // expected, but missing Send

That concrete type is the real fix target.

7. Example corrected setup

use std::sync::{Arc, Mutex};
use wasmtime::component::bindgen;
use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiView};

bindgen!({
    world: "gav",
    with: {
        "wasi": wasmtime_wasi::bindings,
        "wasi:http": wasmtime_wasi_http::bindings::http,
    },
});

struct AppState {
    table: ResourceTable,
    ctx: WasiCtx,
    shared_data: Arc<Mutex<Vec<String>>>,
}

impl WasiView for AppState {
    fn table(&mut self) -> &mut ResourceTable {
        &mut self.table
    }

    fn ctx(&mut self) -> &mut WasiCtx {
        &mut self.ctx
    }
}

fn build_state() -> AppState {
    AppState {
        table: ResourceTable::new(),
        ctx: WasiCtxBuilder::new().build(),
        shared_data: Arc::new(Mutex::new(Vec::new())),
    }
}

This pattern avoids the most common non-Send pitfalls in generated component bindings.

Common Edge Cases

Using tokio::spawn with non-Send futures

If host logic eventually gets spawned onto a multi-threaded executor, the future itself must be Send. Even if your core state looks correct, a borrowed value or temporary guard can still break this.

Trait objects without Send

This pattern often fails:

Box<dyn MyService>

while this is usually required:

Box<dyn MyService + Send>

If shared across threads, you may also need:

Box<dyn MyService + Send + Sync>

Hidden non-thread-safe dependencies

Sometimes your own state is fine, but a nested library type is not. For example, an SDK client may internally use Rc or thread-local state. If the compiler points into a third-party type, check whether that crate provides a thread-safe variant.

Mixing sync assumptions with WASI HTTP integration

WASI HTTP integrations can introduce async execution paths and more aggressive trait bounds than plain component bindings. A custom world may compile until "wasi:http" is added to the with map, at which point Send becomes mandatory.

Resource types stored in state

If your generated bindings manage component resources and your backing store for those resources contains single-threaded internals, the error may show up far away from the original definition. Audit all resource backing types, not just the top-level state struct.

FAQ

Why does this only happen after adding "wasi" or "wasi:http" to with?

Because those bindings often bring in host traits and runtime behavior that require stronger thread-safety guarantees. Your custom world alone may not force Send, but the composed generated output can.

Can I fix this by adding unsafe impl Send manually?

Usually no. That is dangerous unless you completely understand the thread-safety of every field involved. The correct fix is to replace non-thread-safe data structures with truly thread-safe ones such as Arc and Mutex, or redesign ownership so the type naturally satisfies the bound.

How do I quickly find the exact offending type?

Read the full compiler error and look for the first concrete type that does not implement Send. Then search your state and host bindings for that type. If the message is too noisy, run a clean build and inspect the expanded type chain carefully with your IDE or Rust compiler diagnostics.

The shortest path to resolving this issue is simple: treat the generated component::bindgen! code as a mirror of your runtime constraints. If the bindings require Send, make your host state, resource storage, async captures, and trait objects genuinely thread-safe rather than trying to fight the generated code.

Leave a Reply

Your email address will not be published. Required fields are marked *