somdoron opened issue #14286:
When a component whose export is lifted with
asyncblocks in WASI 0.2 (for
examplewasi:io/poll@0.2'spollable.blockorpoll), the whole
Storestops until that call returns. Nothing else driven by
Store::run_concurrentmakes progress in the meantime, and anasync-lowered
call into that component comes back asRETURNEDinstead ofSTARTED. The
same component blocking throughwasi:clocks@0.3wait-forbehaves as
expected: only its own task suspends and other tasks keep running.This matters for mixing WASI 0.2 and 0.3 components in one store: any 0.2
component that waits on I/O effectively serializes everything around it.Test Case
Standalone reproduction (single
cargo run, builds and composes the components
itself): https://github.com/somdoron/wasmtime-p2-block-reproAttached separately for convenience (with a
.txtsuffix so GitHub accepts them):
repro.wit.txt— the WIT package (local:repro) with both worlds. It also needs
wasi:io@0.2.6,wasi:clocks@0.2.6andwasi:clocks@0.3.0-rc-2026-03-15
inwit/deps/.
sleeper-p2.wat.txt— component 1 (worldsleeper-p2):sleepis lifted
async-stackful and blocks in WASI 0.2 viamonotonic-clock.subscribe-duration
pollable.block.main.wat.txt— component 2 (worldmain):gois lifted async-stackful, starts
component 1'ssleep(long)and awasi:clocks@0.3wait-for(short), both
withcanon lower ... async, and waits on both.Each core module is turned into a component with
wit_component::embed_component_metadata+ComponentEncoderagainst its
world, then the two are composed withwac plug(component 1 into component 2).Steps to Reproduce
git clone https://github.com/somdoron/wasmtime-p2-block-repro && cargo run, or by hand:- Build both components from the attached WAT and WIT files and compose them with
wac plug(component 1 into component 2).- Configure the engine with
wasm_component_model_async,wasm_component_model_more_async_builtins,wasm_component_model_async_stackfulandconcurrency_supportenabled.- Add
wasmtime_wasi::p3::add_to_linkerandwasmtime_wasi::p2::add_to_linker_asyncto the linker, plus a host implementation oflocal:repro/logthat prints the events with timestamps.- Instantiate and run
store.run_concurrent(|acc| go.call_concurrent(acc, (300_000_000, 100_000_000))).Expected Results
The async-lowered call into component 1 returns
STARTED, component 1 suspends
on its own task insidepollable.block, the 100ms timer fires first, total
≈300ms.When a thread calls an import using the async ABI, the Component Model
guarantees that if the callee task blocks, control flow is immediately
returned back to the caller's thread.If instead the task has not yet returned a value and the callee's function
type declares theasynceffect, control flow returns directly to the caller.
If the caller used the async ABI, then control flow returns to Core
WebAssembly, indicating that the call "blocked" by returning the non-zero
index of a new subtask.
sleepisasyncin WIT and component 2 lowers it withasync, so once
component 1's task blocks the call must come back to component 2 asSTARTED.
The same section does leave it to the host to decide what counts as blocking
for host-implemented imports ("what counts as 'blocking' is up to the host"),
but Wasmtime already treats the WASI 0.3 equivalent as blocking, and a
300ms timer wait is hard to see as anything else.The spec is also explicit that the WASI 0.2 sync ABI is meant to keep working
inside this model rather than being a separate, non-concurrent world
(Summary
and Goals):The new async ABI can be used alongside or instead of the existing WASI 0.2
"sync ABI" to call or implement anyasync-typed functions. When calling
an imported function via the async ABI, if theasynccallee blocks, control
flow is returned immediately to the caller, and the callee continues
executing concurrently. [...] Critically, both sync-ABI-calls-async-ABI and
async-ABI-calls-sync-ABI pairings have well-defined, composable behavior for
both inter-component and intra-component calls.Avoid partitioning interfaces and components into separate ecosystems based
on degree of concurrency; don't give components a "color".Component 1 here is exactly the "traditional sync code compiled to a component
exportingasyncfunctions" case the Summary describes, except that its sync
imports are WASI 0.2 rather than WASI 0.3.Actual Results
The async-lowered call blocks for the full 300ms and returns
RETURNED. The
100ms timer hasn't even been started yet when it returns. Total ≈400ms.[ 0ms] component 2: go entered [ 1ms] component 1: entered [ 303ms] component 1: woke <-- 300ms, store idle [ 303ms] component 2: LONG (component 1) call status = 2 <-- RETURNED, not STARTED [ 304ms] component 2: SHORT (p3 timer) call status = 17 <-- STARTED, subtask 1 [ 406ms] component 2: EVENT from subtask 1 [ 406ms] component 2: go finished total 406msBlocking in
wasi:io/poll@0.2.6#pollinstead ofpollable.blockbehaves the
same.Versions and Environment
Wasmtime version or commit: 45.0.3; also reproduced on
mainat 0754181e4bOperating system: macOS
Architecture: aarch64
Extra Info
I see two possible directions and would appreciate guidance on which one is
preferred:
Fix it on the host side, so that the blocking
wasi:io@0.2operations in
wasmtime-wasi-iosuspend only the calling task when the caller is allowed
to suspend (and keep blocking the store as today when it isn't).Provide a WASI 0.2 → 0.3 adapter component, similar to the existing
preview1 → 0.2 adapter, implementingwasi:io/poll@0.2and the blocking
wasi:io/streams@0.2operations on top of the 0.3asyncfunctions and
streams, so every wait in the composed component is already anasync
import. This assumes 0.3 exposes enough to build apollablefor every 0.2
resource, which I haven't checked.Happy to work on either.
somdoron added the bug label to Issue #14286.
pchickey commented on issue #14286:
Unfortunately, our project doesn't plan to support either of these options. The solution for users is to use WASI 0.3 interfaces if they need to run stores concurrently.
WASI 0.2 predates component model concurrency, and component model concurrency was designed to improve on many aspects of the WASI 0.2 pollable and stream design. The WASI 0.3 interfaces and implementation have major changes in order to support CM concurrency, and based on our experiences in the 0.1 to 0.2 transition, we have decided that making compatibility between versions with either enhancements to the host (your option 1) or component adapters (option 2) are both very challenging for us to create and maintain, and have significant drawbacks for our users.
Right now, targeting WASI 0.3 from guest code is limited by the maturity of guest toolchain integration. Wasi-sdk now supports 0.3 for C/C++ guests and Rust nightly has a wasm32-wasip3 target as well, and will ride the Rust release trains towards availability in stable at the end of this year. Other language toolchains with p3 support exist as well but I'm not totally up to date on their statuses. So, we advise you approach this problem by upgrading your guests, and if you run into issues with particular toolchains you can file issues and/or contribute fixes there.
somdoron closed issue #14286:
When a component whose export is lifted with
asyncblocks in WASI 0.2 (for
examplewasi:io/poll@0.2'spollable.blockorpoll), the whole
Storestops until that call returns. Nothing else driven by
Store::run_concurrentmakes progress in the meantime, and anasync-lowered
call into that component comes back asRETURNEDinstead ofSTARTED. The
same component blocking throughwasi:clocks@0.3wait-forbehaves as
expected: only its own task suspends and other tasks keep running.This matters for mixing WASI 0.2 and 0.3 components in one store: any 0.2
component that waits on I/O effectively serializes everything around it.Test Case
Standalone reproduction (single
cargo run, builds and composes the components
itself): https://github.com/somdoron/wasmtime-p2-block-reproAttached separately for convenience (with a
.txtsuffix so GitHub accepts them):
repro.wit.txt— the WIT package (local:repro) with both worlds. It also needs
wasi:io@0.2.6,wasi:clocks@0.2.6andwasi:clocks@0.3.0-rc-2026-03-15
inwit/deps/.
sleeper-p2.wat.txt— component 1 (worldsleeper-p2):sleepis lifted
async-stackful and blocks in WASI 0.2 viamonotonic-clock.subscribe-duration
pollable.block.main.wat.txt— component 2 (worldmain):gois lifted async-stackful, starts
component 1'ssleep(long)and awasi:clocks@0.3wait-for(short), both
withcanon lower ... async, and waits on both.Each core module is turned into a component with
wit_component::embed_component_metadata+ComponentEncoderagainst its
world, then the two are composed withwac plug(component 1 into component 2).Steps to Reproduce
git clone https://github.com/somdoron/wasmtime-p2-block-repro && cargo run, or by hand:- Build both components from the attached WAT and WIT files and compose them with
wac plug(component 1 into component 2).- Configure the engine with
wasm_component_model_async,wasm_component_model_more_async_builtins,wasm_component_model_async_stackfulandconcurrency_supportenabled.- Add
wasmtime_wasi::p3::add_to_linkerandwasmtime_wasi::p2::add_to_linker_asyncto the linker, plus a host implementation oflocal:repro/logthat prints the events with timestamps.- Instantiate and run
store.run_concurrent(|acc| go.call_concurrent(acc, (300_000_000, 100_000_000))).Expected Results
The async-lowered call into component 1 returns
STARTED, component 1 suspends
on its own task insidepollable.block, the 100ms timer fires first, total
≈300ms.When a thread calls an import using the async ABI, the Component Model
guarantees that if the callee task blocks, control flow is immediately
returned back to the caller's thread.If instead the task has not yet returned a value and the callee's function
type declares theasynceffect, control flow returns directly to the caller.
If the caller used the async ABI, then control flow returns to Core
WebAssembly, indicating that the call "blocked" by returning the non-zero
index of a new subtask.
sleepisasyncin WIT and component 2 lowers it withasync, so once
component 1's task blocks the call must come back to component 2 asSTARTED.
The same section does leave it to the host to decide what counts as blocking
for host-implemented imports ("what counts as 'blocking' is up to the host"),
but Wasmtime already treats the WASI 0.3 equivalent as blocking, and a
300ms timer wait is hard to see as anything else.The spec is also explicit that the WASI 0.2 sync ABI is meant to keep working
inside this model rather than being a separate, non-concurrent world
(Summary
and Goals):The new async ABI can be used alongside or instead of the existing WASI 0.2
"sync ABI" to call or implement anyasync-typed functions. When calling
an imported function via the async ABI, if theasynccallee blocks, control
flow is returned immediately to the caller, and the callee continues
executing concurrently. [...] Critically, both sync-ABI-calls-async-ABI and
async-ABI-calls-sync-ABI pairings have well-defined, composable behavior for
both inter-component and intra-component calls.Avoid partitioning interfaces and components into separate ecosystems based
on degree of concurrency; don't give components a "color".Component 1 here is exactly the "traditional sync code compiled to a component
exportingasyncfunctions" case the Summary describes, except that its sync
imports are WASI 0.2 rather than WASI 0.3.Actual Results
The async-lowered call blocks for the full 300ms and returns
RETURNED. The
100ms timer hasn't even been started yet when it returns. Total ≈400ms.[ 0ms] component 2: go entered [ 1ms] component 1: entered [ 303ms] component 1: woke <-- 300ms, store idle [ 303ms] component 2: LONG (component 1) call status = 2 <-- RETURNED, not STARTED [ 304ms] component 2: SHORT (p3 timer) call status = 17 <-- STARTED, subtask 1 [ 406ms] component 2: EVENT from subtask 1 [ 406ms] component 2: go finished total 406msBlocking in
wasi:io/poll@0.2.6#pollinstead ofpollable.blockbehaves the
same.Versions and Environment
Wasmtime version or commit: 45.0.3; also reproduced on
mainat 0754181e4bOperating system: macOS
Architecture: aarch64
Extra Info
I see two possible directions and would appreciate guidance on which one is
preferred:
Fix it on the host side, so that the blocking
wasi:io@0.2operations in
wasmtime-wasi-iosuspend only the calling task when the caller is allowed
to suspend (and keep blocking the store as today when it isn't).Provide a WASI 0.2 → 0.3 adapter component, similar to the existing
preview1 → 0.2 adapter, implementingwasi:io/poll@0.2and the blocking
wasi:io/streams@0.2operations on top of the 0.3asyncfunctions and
streams, so every wait in the composed component is already anasync
import. This assumes 0.3 exposes enough to build apollablefor every 0.2
resource, which I haven't checked.Happy to work on either.
somdoron commented on issue #14286:
Thank you, it makes total sense. Closing this.
Last updated: Sep 20 2026 at 18:08 UTC