WebAssembly System Interface Preview 3 (or WASIp3 for short) is the next major release of WASI, a set of standards-track APIs for portable application development with WebAssembly. This release will include significant improvements to the underlying Component Model – including support for first class, composable concurrency – while maintaining compatibility with the existing WASIp2 release.
In this post, we’ll summarize the new features coming with WASIp3, look at the status of the implementation efforts, and finally build a real-world example which puts those new features to use.
About WASI and the WebAssembly Component Model
WASI (WebAssembly System Interface) is a set of interfaces covering both traditional operating system features (e.g. file I/O, clocks, and networking) as well as more specialized features such as cloud services (HTTP, database access, messaging, etc.), abstractions for embedded systems, cryptography, etc.
So far, WASI has had two milestone releases: 0.1 (AKA WASIp1, “Preview 1”) and 0.2 (AKA WASIp2, “Preview 2”). The latter was released early last year and was built upon the Component Model, a proposed standard for expressing the high-level interface of a Wasm application or library, enabling cross-language code reuse and secure dependency isolation.
Before WASIp2 was even released, a few of us in the Bytecode Alliance were already prototyping the foundations of WASIp3, and we’ve made a lot of progress since then. Although there’s still significant work to do before it’s finally released later this year, we expect to start making release candidate snapshots available in the next couple of months, with experimental support for those snapshots landing in Spin and other popular runtimes soon after. These runtimes use WASI to provide a standardized environment for WebAssembly modules to interact with the host system.
New Features in WASIp3
The main theme of WASIp3 is composable concurrency. Earlier releases
support asynchronous I/O via a poll
function which accepts a set of handles
representing in-progress tasks, blocking until at least one of them completes.
That works great for many purposes, but it makes composing components
impractical when more than one of them need to do I/O concurrently; only one can
call poll
at a time, during which none of the others can make progress.
In contrast, WASIp3 is based on two important new Component Model features:
-
An asynchronous function ABI: This allows components to export and/or import functions using either the existing synchronous ABI or the new asynchronous ABI. Additionally, it avoids the function coloring problem by seamlessly connecting async imports to sync-exported functions and vice versa.
-
Support for built-in, generic
stream
andfuture
types, providing efficient, optionally asynchronous, cross-component and component<->host communication.
Together, these features make concurrency a first-class concept in the component model, allowing arbitrary numbers of guest tasks to run concurrently in the same component instance. Moreover, components using the new async ABI may be composed with other components without risk of being blocked when one of those components makes a synchronous call. In that case, the host runtime will notice the blocking call, suspend the task that made it, and allow the original component to continue executing.
Thanks to those new features, the WebAssembly Interface Types (WIT) definitions
for 0.3 have been significantly simplified compared to their 0.2 counterparts.
For example, while
wasi:http@0.2.4
includes 11 distinct resource types,
wasi:http@0.3.0-draft
needs only 5. Similarly, the ceremony required to perform an asynchronous
operation with WASIp2 – creating a pollable
, adding it to a list along with
anything else you intend to wait for, calling poll
, and dispatching events
according to which pollable
s are ready – has been replaced with a single call
to an async
function in your programming language of choice.
WASIp3 Status
The Component Model Specification includes a complete description of the async ABI, streams, futures, and associated built-in functions (look for the 🔀 icon). Note that the spec is still being refined as we gain more implementation experience, but we don’t expect any major changes between now and the WASIp3 release.
Draft 0.3 interfaces have been written for each of the main WASI projects:
Implementation-wise, the latest
wasm-tools
release includes
full support for parsing, emitting, printing, etc. components using the new
async features. Likewise, the latest
wit-bindgen
release
supports generating idiomatic Rust bindings that use those features, with
support for other languages on the way.
Host runtime support for the new Component Model features and WASIp3 is being
developed in the wasip3-prototyping
repo for Wasmtime and
in the jco
repo.
wasi-http
Middleware Example
Let’s see what it’s like to use WASIp3 by writing a simple wasi:http
request
handler in Rust, then composing it with a middleware component which
transparently compresses the response body.
First, we’ll install a few tools to help us build and run the components. Given that the WASIp3 implementation work is still ongoing as of this writing, we’ll use specific versions of these tools which are known to work together. Later versions might work, but might use an incompatible snapshot of the specification.
🚧 Note that, since p3 is a work-in-progress, much of the usual tooling and ergonomic affordances are not yet in place. That means there’s currently extra work and boilerplate needed to build WASIp3 components compared to WASIp2. In this example, we’ll use the 🚧 icon to point out the extra scaffolding which will become unecessary as we get closer to a release.
The following commands assume a Unix-style shell (e.g. Linux, MacOS, or WSL2)
with curl
and git
installed. These commands will install Rust,
wasm-tools
, a temporary fork of Spin with wasi:http@0.3.0-draft
support, a
copy of the corresponding WIT files we’ll use to generate bindings, and an
adapter module we’ll use when generating the components.
🚧 Most of these steps will be unnecessary once wasm32-wasip3
is an offical
Rust target platform and the wasi
crate supports it. 🚧
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target install wasm32-wasip1
cargo install --locked --version 1.227.1 wasm-tools
cargo install --locked --git https://github.com/dicej/spin --branch wasi-http-p3-demo spin-cli
git clone https://github.com/WebAssembly/wasi-http -n && (cd wasi-http && git checkout 505ebdb9)
curl -OL https://github.com/bytecodealliance/wasmtime/releases/download/v30.0.2/wasi_snapshot_preview1.reactor.wasm
Next, we’ll write a “hello, world” wasi:http
request handler, creating three
files:
spin.toml
: the manifest file describing our Spin application:
spin_manifest_version = 2
application.name = "hello"
[[trigger.http]]
route = "/..."
component = "hello"
[component.hello]
source = "hello.wasm"
# 🚧 The `wasm-tools component new` command will be unnecessary once Rust has
# proper `wasm32-wasip3` support. 🚧
build.command = """
cargo build --manifest-path hello/Cargo.toml --release --target wasm32-wasip1 \
&& wasm-tools component new --skip-validation \
hello/target/wasm32-wasip1/release/hello.wasm \
-o hello.wasm --adapt wasi_snapshot_preview1.reactor.wasm
"""
hello/Cargo.toml
: the project and dependency configuration file for our Rust project:
[package]
name = "hello"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies.wit-bindgen]
git = "https://github.com/bytecodealliance/wit-bindgen"
rev = "d73c073b"
[dependencies.wit-bindgen-rt]
git = "https://github.com/bytecodealliance/wit-bindgen"
rev = "d73c073b"
hello/src/lib.rs
: the source code for our component:
// 🚧 Once the `wasi` crate supports WASIp3, we'll be able to use its
// pre-generated bindings rather then generate them on-the-fly with
// `wit-bindgen` as we do here: 🚧
wit_bindgen::generate!({
path: "../wasi-http/wit-0.3.0-draft",
world: "wasi:http/proxy",
async: {
exports: [
"wasi:http/handler@0.3.0-draft#handle",
]
},
generate_all,
});
use {
wasi::http::types::{Body, ErrorCode, Headers, Request, Response},
wit_bindgen_rt::async_support::{self, futures::SinkExt},
};
struct Component;
export!(Component);
impl exports::wasi::http::handler::Guest for Component {
async fn handle(_request: Request) -> Result<Response, ErrorCode> {
let (mut tx, rx) = wit_stream::new();
async_support::spawn(async move {
_ = tx.send(b"Hello, WASIp3!".into()).await;
});
Ok(Response::new(Headers::new(), Some(Body::new(rx).0)))
}
}
Now we can build that and run it:
spin build -u
Then, in another terminal, we can send a request:
curl -i localhost:3000
If all went well, we should see something like:
HTTP/1.1 200 OK
transfer-encoding: chunked
date: Tue, 11 Mar 2025 16:38:58 GMT
Hello, WASIp3!
Next, we’ll write another component which we can compose with the first to add support for compressing the response body. We’ll create two new files:
middleware/Cargo.toml
: the project and dependency configuration file for our Rust project:
[package]
name = "middleware"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies.wit-bindgen]
git = "https://github.com/bytecodealliance/wit-bindgen"
rev = "d73c073b"
[dependencies.wit-bindgen-rt]
git = "https://github.com/bytecodealliance/wit-bindgen"
rev = "d73c073b"
[dependencies.flate2]
version = "1.0.28"
middleware/src/lib.rs
: the source code for our component:
// 🚧 Once the `wasi` crate supports WASIp3, we'll be able to use its
// pre-generated bindings rather then generate them on-the-fly with
// `wit-bindgen` as we do here: 🚧
wit_bindgen::generate!({
path: "../wasi-http/wit-0.3.0-draft",
world: "wasi:http/proxy",
async: {
imports: [
"wasi:http/handler@0.3.0-draft#handle",
],
exports: [
"wasi:http/handler@0.3.0-draft#handle",
]
},
generate_all,
});
use {
flate2::{write::DeflateEncoder, Compression},
std::{io::Write, mem},
wasi::http::{
handler,
types::{Body, ErrorCode, Headers, Request, Response},
},
wit_bindgen_rt::async_support::{
self,
futures::{SinkExt, StreamExt},
},
};
struct Component;
export!(Component);
impl exports::wasi::http::handler::Guest for Component {
/// Forward the specified request to the imported `wasi:http/handler`,
/// encoding the response body if the client has provided an
/// `accept-encoding: deflate` header.
async fn handle(request: Request) -> Result<Response, ErrorCode> {
// 🚧 Since we're using the auto-generated bindings directly, the code
// below is a bit verbose. We expect crates like
// [wstd](https://docs.rs/wstd) will provide types, traits, and
// functions to help make code like this more concise. 🚧
// First, extract the parts of the request and check for (and remove)
// the `accept-encoding` header.
let method = request.method();
let scheme = request.scheme();
let authority = request.authority();
let path_with_query = request.path_with_query();
let (headers, body) = Request::into_parts(request);
let mut headers = headers.entries();
let mut accept_deflated = false;
headers.retain(|(key, value)| {
if "accept-encoding" == key.as_str() {
accept_deflated |= std::str::from_utf8(&value)
.map(|v| v.contains("deflate"))
.unwrap_or(false);
false
} else {
true
}
});
// Synthesize a request from the parts collected above and pass it to
// the imported `wasi:http/handler`.
let inner_request = Request::new(
Headers::from_list(&headers).unwrap(),
body,
None
);
inner_request.set_method(&method).unwrap();
inner_request.set_scheme(scheme.as_ref()).unwrap();
inner_request
.set_path_with_query(path_with_query.as_deref())
.unwrap();
inner_request.set_authority(authority.as_deref()).unwrap();
let response = handler::handle(inner_request).await?;
// Now that we have the response, extract the parts and optionally
// compress the body if applicable.
let status_code = response.status_code();
let (headers, body) = Response::into_parts(response);
let mut headers = headers.entries();
let body = if accept_deflated {
if let Some(body) = body {
// Remove the `content-length` header, if any, since we'll be
// using chunked encoding instead.
headers.retain(|(name, _value)| name != "content-length");
// Add a header to indicate the encoding.
headers.push(("content-encoding".into(), b"deflate".into()));
// Spawn a task to pipe and encode the original response body and
// trailers into a new response we'll create below. This will run
// concurrently with the caller's code (i.e. it won't necessarily
// complete before we return a value).
let (mut pipe_tx, pipe_rx) = wit_stream::new();
async_support::spawn(async move {
let mut body_rx = body.stream().unwrap().0;
let mut encoder = DeflateEncoder::new(
Vec::new(),
Compression::fast()
);
while let Some(Ok(chunk)) = body_rx.next().await {
encoder.write_all(&chunk).unwrap();
pipe_tx.send(mem::take(encoder.get_mut())).await.unwrap();
}
pipe_tx.send(encoder.finish().unwrap()).await.unwrap();
});
Some(Body::new(pipe_rx).0)
} else {
None
}
} else {
body
};
// While the above task (if any) is running, synthesize a response from
// the parts collected above and return it.
let response = Response::new(Headers::from_list(&headers).unwrap(), body);
response.set_status_code(status_code).unwrap();
Ok(response)
}
}
And then we’ll edit the spin.toml
file we created earlier to build our new
component and compose it with the original one:
spin_manifest_version = 2
application.name = "hello"
[[trigger.http]]
route = "/..."
component = "hello"
[component.hello]
source = "composed.wasm"
# 🚧 The `wasm-tools component new` commands will be unnecessary once Rust has
# proper `wasm32-wasip3` support. 🚧
build.command = """
cargo build --manifest-path hello/Cargo.toml \
--release --target wasm32-wasip1 \
&& wasm-tools component new --skip-validation \
hello/target/wasm32-wasip1/release/hello.wasm \
-o hello.wasm --adapt wasi_snapshot_preview1.reactor.wasm \
&& cargo build --manifest-path middleware/Cargo.toml \
--release --target wasm32-wasip1 \
&& wasm-tools component new --skip-validation \
middleware/target/wasm32-wasip1/release/middleware.wasm \
-o middleware.wasm --adapt wasi_snapshot_preview1.reactor.wasm \
&& wasm-tools compose --skip-validation middleware.wasm -d hello.wasm \
-o composed.wasm
"""
Now we can build and run the composition:
spin build -u
Then, in another terminal, we can send a request:
curl --compressed -i localhost:3000
If all went well, we should see something like:
HTTP/1.1 200 OK
content-encoding: deflate
transfer-encoding: chunked
date: Tue, 11 Mar 2025 16:52:13 GMT
Hello, WASIp3!
Note that, while we used Rust to write both of the above components, we could just as easily have written one of them in Python and the other in JavaScript – the Component Model enables language-agnostic code reuse, so you can use the best tool for each job and easily reuse third-party components regardless of their implementation language.
In addition, although we’ve used the wasi:http
WIT files to define the
interface between the components we built, we could have just as easily written
our own custom, domain-specific WIT file and used that instead. In that case,
we’d have all the same Component Model features at our disposal:
- A rich type system supporting custom
record
,variant
, andresource
types, etc. future
s andstream
s (with arbitrary payload types) representing deferred computation and I/O- Optionally asynchronous imports and exports
- Cross-language composition
- Secure sandboxing
Next Steps
Although WASIp3 has not yet been released, and the implementations are not yet ready for production use, now is a great time to experiment with it and provide feedback by opening issues on either the spec repo or the wasip3-prototyping repo. Whether you’re developing Wasm components or custom host embeddings to run components, we’d love to have your input. And if you’re a language implementer looking to add concurrency support for Wasm targets, we’d appreciate your input as well.
Finally, if you’re interested in helping us finish and ship WASIp3, don’t hesitate to reach out on the WASI Zulip channel, and check out the project board to see what work remains to do.
Acknowledgements
The design and implementation of WASIp3 has been a collaborative effort from the beginning. Here are a few of the more notable contributors:
- Luke Wagner @Fastly (design, specification, and Python reference implementation)
- Bailey Hayes, Roman Volosatovs, and Victor Adossi @Cosmonic (WIT updates, WASIp3 host implementation, Jco implementation, stream/future error handling)
- Tomasz Andrzejak and Yoshua Wuyts @Microsoft (Jco implementation, Cat herding)
- Calvin Prewitt @JAF Labs (Jco implementation)
- Alex Crichton and Joel Dice @Fermyon (wasm-tools, wit-bindgen, and Wasmtime implementations)