WASI preview 3: threads and async in WebAssembly

Líneas de código abstractas formando flujos paralelos sobre fondo oscuro

Over the past few years, WebAssembly outside the browser has grown slowly but with direction. The big bottleneck has always been the same: the concurrency model. Preview 1 had nothing resembling threads or async; preview 2 opened the door to the component model but left concurrency as unfinished business. Preview 3, which has been in final standardization for months, is finally the missing piece, and it’s worth understanding why it matters.

This post isn’t meant to be an exhaustive tutorial of the API, but a mental map: what problem preview 3 solves, how it addresses it, and what implications it has for languages and platforms adopting it.

The problem we were dragging

Before preview 3, running concurrent WebAssembly code outside the browser required external hacks. Runtimes (Wasmtime, WasmEdge) invented their own extensions to offer multi-threading, and each language compiled to Wasm had to make do. Rust compiled to wasm32-unknown-unknown without thread support, and if you wanted threads, you needed specific runtimes and manually orchestrated shared memory techniques.

The result was a platform that sold portability but didn’t deliver it when your application needed to do two things at once. For any serious workload (an HTTP server, a stream processor, a pipeline with natural concurrency), you had to choose between sticking to synchronous code or jumping outside the standard.

The model preview 3 proposes

Preview 3 introduces what the standard calls structured concurrency, and it’s more interesting than “threads and async”. Instead of offering OS-level low-level primitives (mutex, conditions, locks), preview 3 lifts the model to composable primitives: futures and streams.

A future in preview 3 is exactly what you think: an operation that hasn’t finished yet, but promises to produce a value. A stream is a sequence of values arriving over time, not all at once. With those two primitives, plus the notion of tasks, you can build practically any known concurrency pattern, from an async HTTP call to a streaming processing pipeline.

What’s relevant is that these primitives are part of the component model, not the runtime. That means a language compiling to Wasm can offer its own async/await syntax and translate it to component-model futures in the final binary. And when that binary interacts with another component (potentially written in another language), concurrency crosses the boundary cleanly.

Why it matters

The real value of preview 3 isn’t that Wasm can do async, but that it can do so in a standard way across language boundaries. This has several practical consequences.

First, an HTTP server written in Rust can call, within the same binary, a Wasm module written in Go that processes streams. The module call can be asynchronous, and the data flow between them can be streaming. Before, either you were trapped in the same implementation (all Rust) or you made the boundary synchronous (which kills performance for intensive workloads).

Second, platforms running WebAssembly in production (Fastly Compute, Cloudflare Workers in some experimental mode, Fermyon Spin, several serverless functions) can now support async patterns natively, without platform-specific hacks. This is a big step toward real portability of Wasm serverless apps across providers.

Third, and perhaps most important long-term, language-specific SDKs will converge. Today, writing a Lambda function in Rust implies an SDK that exposes its own async primitives; in Go, different ones; in Python, others. With preview 3, the standard pushes all those SDKs to speak the same model, which reduces fragmentation and makes code easier to port between platforms.

What’s still unresolved

Despite progress, preview 3 leaves some things pending.

Mapping these primitives to each runtime’s native threading model remains the runtime’s responsibility. That means exact behavior (how many real threads get created? how are tasks scheduled?) can vary between Wasmtime, Wasmer, and others. In practice, running the same heavily concurrent component on two different runtimes can yield very different performance, even if correctness is guaranteed.

Interop with host-native code (functions exposed from the runtime to the Wasm component) also needs more work. The standard defines how hosts should behave, but edge cases, like cancellation during an async operation, are still being polished.

And of course, adoption in languages. Rust is on a good path with wasm-bindgen work and experimental preview 3 branches. Go, Python, and C++ are behind, with variable states. Giving the ecosystem time to converge will probably take all of 2025.

What this means for developers

If you work on an app already using WebAssembly outside the browser, preview 3 is good news: it will simplify code that today depends on runtime-specific hacks. If you’re starting to explore Wasm as an alternative to containers for certain cases (serverless functions, embedded plugins, strong isolation at low cost), preview 3 is the version where starting to take it seriously.

If you haven’t touched Wasm at all, this is still a good time to look at it. The platform has matured enough that the early rough edges aren’t the obstacle anymore. Preview 3 closes the “yes, but…” chapter it had been dragging for years: yes, Wasm works, yes, you can use it seriously, yes, you can do decent concurrency.

My intuition is that the next twelve months will define whether WebAssembly becomes a mainstream deployment platform for serverless services or stays a specialized niche. Preview 3 is probably the piece that tips the balance, and that’s why it’s worth understanding now, even if it isn’t tomorrow that you’ll use it in production.

Entradas relacionadas