Hi, I am trying to build a system where components run in their own thread. Since Store is not thread safe, I can’t call into the component from outside the thread. I was reading the posts about run_concurrent and call_concurrent and they seem very promising for my use case.
The design would be something like calling my component’s entry point with call_concurrent and then using a select! loop to receive messages from the host/other threads for my component to process. Reading the documentation for StoreContextMut::run_concurrent (https://docs.wasmtime.dev/api/wasmtime/struct.StoreContextMut.html#method.run_concurrent), though, it seems like this is actually not working in the current implementation of wasmtime. Is that correct?
I was also wondering about cancellation safety of the future returned by call_concurrent. tokio::select! cancels remaining futures when one is ready. But that would drop the Store right? For example something like:
let run = instance.get_typed_func(…);
let entry_fut = run.call_concurrent(...);
loop {
tokio::select! {
_ = async { entry_fut.await } => { break }, // async block to avoid .await/cancel directly on entry_fut, does this work?
cmd = command_queue.recv() => …,
msg = message_queue.recv() => …,
}
}
Would the above result in the store getting dropped due to entry_fut being cancelled when one of the queues is ready?
Is there a better way to go about this?
Jonathan H said:
Reading the documentation for
StoreContextMut::run_concurrent(https://docs.wasmtime.dev/api/wasmtime/struct.StoreContextMut.html#method.run_concurrent), though, it seems like this is actually not working in the current implementation ofwasmtime. Is that correct?
That documentation is referring to https://github.com/bytecodealliance/wasmtime/issues/11869 and https://github.com/bytecodealliance/wasmtime/issues/11870. Traditionally, an "async host function" in Wasmtime meant a function which returned a Future which closed over the StoreContextMut and thus had exclusive access to it until that Future was dropped. That was fine for Futuress which wanted to yield to the host embedder (e.g. to do other work not involving that specific store). However, it meant that the store could not be used for any other purpose until the Future was dropped, i.e. no Wasm code could be run in instances owned by that store, etc.
Clearly that model wouldn't work for the new component model async ABI, where the whole point was to allow multiple concurrent tasks to execute and be interleaved, and where host functions should only (at most) block the caller, but not other tasks. So we added a new Linker::func_wrap_concurrent function for registering host functions. Unlike the existing func_wrap_async function, func_wrap_concurrent takes a closure which returns a Future which only has exclusive access to the store between await points but not across await points. That means it effectively yields control of the store whenever it awaits, allowing that store to be used for other work, e.g. running other tasks owned by the store.
As you would expect, wasmtime-wasi[-http]'s WASIp3 implementation uses func_wrap_concurrent rather than func_wrap_async. However, its WASIp2 implementation remains unchanged -- it's still using func_wrap_async. That means any component using WASIp2 APIs (or custom host functions implemented using func_wrap_async) can potentially monopolize the entire store, starving the event loop that drives run_concurrent. In addition, the epoch interruption mechanism is still using something akin to func_wrap_async and also monopolizes the store.
Both of those issues are entirely fixable, but will take some time to do. Meanwhile, you can absolutely use tokio::select! inside run_concurrent; just keep in mind that nothing in the closure you pass to run_concurrent (inside or outside a tokio::select!) can run as long as a call to a host function added using func_wrap_async is pending. Alternatively, if your component only uses WASIp3 imports and/or custom host functions added using func_wrap_concurrent, and doesn't use epoch interruption callbacks which await, then there's no chance run_concurrent will be starved. And once we've addressed those issues, you won't have to worry about any of this.
Jonathan H said:
I was also wondering about cancellation safety of the future returned by
call_concurrent.tokio::select!cancels remaining futures when one is ready. But that would drop theStoreright?
No, it won't drop the store. The Future returned by call_concurrent does not own the store, and it will only have exclusive access to the store between await points. That's what allows it to run concurrently with other in-progress tasks created using call_concurrent. Furthermore, dropping that Future will not drop the task it represents. There's currently no public API for cancelling tasks (that's a TODO item), so that task will continue to run until either it finishes or the closure you passed to run_concurrent returns. The only effect dropping that Future will have is that you'll have no way to retrieve the result returned by the task, if any.
Jonathan H said:
Is there a better way to go about this?
What you've sketched looks reasonable to me.
Thank you so much for the incredibly detailed response. I’m going to move forward with what I have and also see if I can migrate to WASIp3.
Thanks so much for the incredible work on wasmtime and the willingness and time you put into helping users like me.
Last updated: Jan 09 2026 at 13:15 UTC