The WASI Component Model: WebAssembly's Missing Piece
WebAssembly modules are islands. Each one gets a flat block of linear memory — bytes with addresses. When you want to call a function in another module, you pass integers and floats. You want to send a string? Pass a pointer (an integer) and a length (another integer) and hope both sides agree on the encoding. Want to return a list of structs? Better have a memory-allocation protocol documented somewhere. In practice, this worked well enough for browser use cases — JS held everything together. But for server-side polyglot architectures, it was a fundamental blocker.
The WASI Component Model is the specification that solves this. It defines a standard for composing WebAssembly modules with rich, typed interfaces — strings, lists, records, variants, resources — across language boundaries without any serialization overhead or out-of-band protocol negotiation. It is also the foundation of WASI Preview 2, the current generation of the WebAssembly System Interface.
This post goes deep. We will cover the problem, the specification, the WIT interface definition language, the toolchain, wasmtime, Fermyon Spin, component composition, plugin systems, the polyglot story, and an honest assessment of what is ready for production today.
The Interface Problem
What WebAssembly Modules Can Actually Exchange
A WebAssembly module in the traditional (pre-Component-Model) sense — called a core module — communicates through a handful of primitive types:
i32,i64— 32-bit and 64-bit integersf32,f64— 32-bit and 64-bit floatsv128— 128-bit vector (SIMD)funcref,externref— reference types (table entries)
That is the entire type system at the module boundary. There are no strings. No arrays. No structs. No booleans as a type. No null. Everything you want to pass between modules must be encoded as integers.
In practice, passing a string from module A to module B looks like this: module A allocates memory in its linear memory space, writes the UTF-8 encoded bytes of the string, then passes two integers to module B — a pointer (offset into linear memory) and a byte length. Module B then has to read from that same linear memory.
There are two immediate problems:
Problem 1: Shared linear memory. For module B to read the string that module A wrote, they need to share the same linear memory. In the WASM 1.0 model, each module has its own memory by default. There are two workarounds: import the memory from the host (both modules share a single memory provided by the embedder), or use the proposed shared-memory proposal. Both have serious implications — shared memory between untrusted components breaks isolation.
Problem 2: No standard ABI. Even if you solve the memory sharing problem, you still need both sides to agree on how to lay out complex types. Where does the string go in memory? Who allocates it? Who frees it? What about a list of strings? A struct containing a list of strings and an optional integer? Every language and framework invents its own conventions. C and Rust have different ABIs. Go uses different calling conventions entirely.
This was addressed in practice by frameworks like Emscripten (which compiles C++ to WASM and generates JavaScript glue code), wasm-bindgen (which does the same for Rust-to-JavaScript), and various host-specific SDKs. But these solutions are pairwise — they work for one specific source language calling one specific target environment. They do not generalize to arbitrary language-to-language communication.
The Shared-Nothing Model
The Component Model takes a philosophically different approach: shared nothing. Components do not share memory with each other. Period. When component A calls component B, B cannot read A’s memory. The call boundary enforces isolation.
This is unlike traditional native linking (where two .o files get linked into one process that shares memory), unlike container networking (where two containers communicate through the OS network stack), and unlike gRPC (where communication goes through a TCP connection and protobuf serialization, also through the OS).
The Component Model achieves shared nothing while still allowing efficient cross-component calls through the Canonical ABI — a specification for how complex types are lowered into the primitive integer types that cross the WASM boundary, how memory is allocated and freed for this purpose, and how strings and lists are passed by value (copied) across component boundaries without sharing memory.
Copying sounds expensive, but consider what you are replacing: if the alternative is a gRPC call with TCP, TLS, HTTP/2 framing, and protobuf encoding/decoding, copying a few hundred bytes of string data is dramatically cheaper. The Component Model is designed for in-process composition where components run in the same runtime instance.
The Versioning and Interface Documentation Problem
Beyond the technical ABI problem, there was a social problem: how do you document what a WASM module expects from its environment? WASI Preview 1 published its interface as a set of .witx files (an earlier format). But for application-level interfaces — your plugin interface, your service interface, your library interface — there was no standard IDL (interface definition language).
Different projects invented different solutions. Extism used a JSON-based approach. Various WASM plugin systems just documented the expected export names and memory conventions in prose. This made tooling nearly impossible: no code generation, no validation, no automatic binding generation.
The Component Model’s answer to this is WIT — WebAssembly Interface Types — a proper IDL that serves as the single source of truth for component interfaces.
What the Component Model Is
The WebAssembly Component Model is a specification developed by the Bytecode Alliance (the consortium that includes Mozilla, Microsoft, Fastly, Intel, Amazon, Cisco, and others) under the WebAssembly CG (Community Group). It consists of several interrelated specifications:
The Component Binary Format: An extension to the WASM binary format that allows a component to be a self-describing package that contains core modules plus metadata about what it imports and exports. A component file has a .wasm extension like a regular module, but its binary header and structure are different. Components can contain other components.
WIT (WebAssembly Interface Types): The IDL for describing component interfaces. WIT files describe interfaces (named sets of functions and types), worlds (the combination of a component’s imports and exports), and packages (collections of related interfaces). WIT is the lingua franca of the Component Model — the source of truth from which language bindings are generated.
The Canonical ABI: The specification for how WIT types map to the primitive types that cross WASM boundaries. The Canonical ABI defines exactly how a string is passed (a pointer and length, where the encoding is always UTF-8), how a list<T> is passed (a pointer and element count), how an option<T> is represented (a flag integer plus the value), how result<T, E> is represented, and so on. Every runtime that implements the Component Model must implement the same Canonical ABI.
WASI Preview 2: The set of standard system interfaces defined using WIT and the Component Model. Where WASI Preview 1 defined POSIX-like interfaces using the older WITX format, WASI Preview 2 defines interfaces like wasi:filesystem, wasi:http, wasi:sockets, and wasi:cli using WIT. WASI Preview 2 shipped as stable in early 2024.
Components vs. Core Modules
It is important to understand the distinction:
A core module is a standard WASM binary. It is what rustc --target wasm32-unknown-unknown or tinygo produce by default. It has imports and exports, but they are all in terms of the primitive WASM types. Core modules can be used directly by runtimes, and they can be embedded inside components.
A component is a higher-level construct. It wraps one or more core modules (or other components) and describes its interface in terms of WIT types. A component knows that its export process-request takes a record { method: string, path: string, headers: list<tuple<string, string>> } and returns a result<record { status: u32, body: list<u8> }, string>. A runtime that loads a component can verify this statically, generate host bindings for it, and compose it with other components.
You can think of the relationship like this: core modules are to components as C object files are to shared libraries with a stable ABI. The component is the deployable artifact with a documented, typed interface.
┌─────────────────────────────────────────────┐
│ Component (.wasm) │
│ │
│ Imports: wasi:http/incoming-handler │
│ wasi:keyvalue/store │
│ │
│ Exports: myapp:api/handler │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Core Module (lowered Rust) │ │
│ │ Linear Memory: [................] │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
WIT: WebAssembly Interface Types
WIT is a purpose-built interface definition language. It is intentionally simple and easy to parse — designed to be read by both humans and code generators.
Basic Structure: Packages, Interfaces, Worlds
// A WIT file belongs to a package
// Format: namespace:name@version
package myorg:blog-platform@0.1.0;
// An interface is a named collection of types and functions
interface posts {
// Type definitions
record post-summary {
id: string,
title: string,
slug: string,
published-at: option<u64>,
tag-count: u32,
}
record post-content {
summary: post-summary,
body: string,
tags: list<string>,
author: string,
}
variant post-error {
not-found(string),
forbidden,
invalid-input(string),
internal(string),
}
// Function definitions
get-post: func(slug: string) -> result<post-content, post-error>;
list-posts: func(limit: u32, offset: u32) -> list<post-summary>;
create-post: func(content: post-content) -> result<string, post-error>;
delete-post: func(id: string) -> result<_, post-error>;
}
// A world describes a complete component's interface:
// what it imports and what it exports
world blog-handler {
// Import system interfaces from WASI
import wasi:http/types@0.2.0;
import wasi:logging/logging@0.1.0;
// Import our application interface
import posts;
// Export the HTTP handler interface
export wasi:http/incoming-handler@0.2.0;
}
The package declaration at the top (myorg:blog-platform@0.1.0) gives the package a unique identity. The namespace (myorg) and name (blog-platform) together identify the package. The version (0.1.0) follows semver.
An interface block defines a named group of types and functions. An interface can be imported or exported by a world.
A world is the complete description of a component’s contract with the outside world. import means the component needs this from the runtime or from other components. export means the component provides this for the runtime or other components to call.
The WIT Type System
WIT has a rich type system that covers all common data patterns:
package myorg:type-showcase@0.1.0;
interface all-the-types {
// Primitive types
demo-primitives: func(
a: bool,
b: u8,
c: u16,
d: u32,
e: u64,
f: s8, // signed 8-bit
g: s16,
h: s32,
i: s64,
j: f32,
k: f64,
l: char, // Unicode scalar value
m: string,
) -> _;
// Compound types
demo-compounds: func(
a: list<u8>, // byte array / buffer
b: list<string>, // list of strings
c: tuple<string, u32>, // fixed-size heterogeneous tuple
d: option<string>, // nullable/optional value
e: result<string, string>, // success or error
f: result<u32, _>, // result with no error payload
g: result<_, string>, // result with no success payload
) -> _;
// Named compound types
record point {
x: f64,
y: f64,
}
record person {
name: string,
age: u32,
email: option<string>,
roles: list<string>,
}
// Variant: like a Rust enum with payloads
variant shape {
circle(f64), // radius
rectangle(tuple<f64, f64>), // width, height
polygon(list<point>), // vertices
unknown, // no payload
}
// Enum: variant with no payloads (C-style enum)
enum log-level {
trace,
debug,
info,
warn,
error,
}
// Flags: a bitfield / set of boolean flags
flags permissions {
read,
write,
execute,
admin,
}
// Type aliases
type byte-array = list<u8>;
type error-message = string;
type maybe-string = option<string>;
// Functions using the above types
compute-area: func(s: shape) -> f64;
check-permissions: func(p: permissions, required: permissions) -> bool;
log-message: func(level: log-level, msg: string) -> _;
}
Resources: Handles to Stateful Objects
Resources are the WIT mechanism for representing stateful, handle-based objects — things that have identity and lifetime, like open file handles, database connections, or active streams:
package myorg:database@0.1.0;
interface db {
// A resource is like a class with methods
resource connection {
// Constructor (returns an owned handle)
constructor(dsn: string);
// Instance methods
execute: func(sql: string, params: list<string>) -> result<u64, string>;
query: func(sql: string, params: list<string>) -> result<list<row>, string>;
// Transaction management
begin: func() -> result<transaction, string>;
// Check if connection is alive
ping: func() -> bool;
}
resource transaction {
execute: func(sql: string, params: list<string>) -> result<u64, string>;
commit: func() -> result<_, string>;
rollback: func() -> result<_, string>;
}
record row {
columns: list<tuple<string, column-value>>,
}
variant column-value {
null,
integer(s64),
real(f64),
text(string),
blob(list<u8>),
}
}
world database-service {
export db;
}
Resources are reference-counted handles. When a resource goes out of scope (determined by the Canonical ABI’s drop mechanism), the component that owns the resource can perform cleanup. Resources passed across component boundaries become “borrowed” handles — the receiving component can call methods on them but does not own them and cannot hold onto them after the call returns (unless it explicitly duplicates the resource, if supported).
Using Interfaces Across Files
Larger WIT definitions are typically split across multiple files using use:
// wit/types.wit
package myorg:platform@0.2.0;
interface common-types {
type user-id = string;
type timestamp = u64;
record pagination {
limit: u32,
offset: u32,
}
variant api-error {
not-found,
unauthorized,
forbidden,
bad-request(string),
internal-error(string),
rate-limited(u32), // retry-after seconds
}
}
// wit/users.wit
package myorg:platform@0.2.0;
// Import types from another interface in the same package
use common-types.{user-id, timestamp, api-error, pagination};
interface users {
record user {
id: user-id,
username: string,
email: string,
created-at: timestamp,
last-login: option<timestamp>,
}
get-user: func(id: user-id) -> result<user, api-error>;
list-users: func(page: pagination) -> result<list<user>, api-error>;
update-email: func(id: user-id, email: string) -> result<_, api-error>;
delete-user: func(id: user-id) -> result<_, api-error>;
}
// wit/world.wit
package myorg:platform@0.2.0;
// Import interfaces from other packages using full path notation
use wasi:http/types@0.2.0.{incoming-request, outgoing-response};
world user-service {
import wasi:http/types@0.2.0;
import wasi:logging/logging@0.1.0;
import wasi:keyvalue/store@0.2.0-draft;
export users;
export wasi:http/incoming-handler@0.2.0;
}
The directory structure for this package:
wit/
├── deps/
│ ├── wasi-http/
│ │ └── types.wit
│ ├── wasi-logging/
│ │ └── logging.wit
│ └── wasi-keyvalue/
│ └── store.wit
├── types.wit
├── users.wit
└── world.wit
The deps/ directory contains WIT files for external packages you depend on. The wasm-tools and cargo-component tooling understands this layout.
wasm-tools and the Toolchain
wasm-tools is the Swiss Army knife of the Component Model toolchain. It is a CLI built and maintained by the Bytecode Alliance.
Installation
|
|
Core wasm-tools Commands
|
|
wit-bindgen: Generating Language Bindings
wit-bindgen reads WIT files and generates source code in the target language that handles all the Canonical ABI machinery for you. You write normal-looking code; the generated bindings handle the integer-level ABI.
|
|
Rust: cargo-component
The primary Rust workflow uses cargo-component, which wraps cargo and handles the build pipeline automatically:
|
|
The Cargo.toml for a component looks like:
|
|
A minimal WIT world for a component:
// wit/world.wit
package myorg:my-service@0.1.0;
world my-service {
// Import WASI interfaces we need
import wasi:logging/logging@0.1.0;
// Export our interface
export run: func() -> result<_, string>;
}
The corresponding Rust implementation:
|
|
The bindings module is auto-generated by cargo component build. You never write the Canonical ABI marshaling code — the toolchain generates it from the WIT definition.
Python: componentize-py
|
|
|
|
For a more complex example with HTTP handling:
|
|
JavaScript/TypeScript: jco
jco (JavaScript Component Tools) handles the bidirectional bridge between the Component Model and JavaScript:
|
|
TypeScript source for a component:
|
|
Go: wit-bindgen-go
Go support is newer but functional:
|
|
|
|
wasmtime: The Reference Runtime
wasmtime is the reference implementation of the WebAssembly runtime from the Bytecode Alliance. It is written in Rust and supports the Component Model natively.
Installation
|
|
wasmtime serve: HTTP Components
A key feature of wasmtime is first-class support for running HTTP components — components that implement the wasi:http/incoming-handler interface:
|
|
The component implements the standard WASI HTTP interface, which means it is portable — the same .wasm file that runs with wasmtime serve also runs in Spin, in Fermyon Cloud, in any WASI-HTTP-compatible host.
Embedding wasmtime in Rust
For applications that need to host WASM components (plugin systems, multi-tenant platforms, extensible services), you embed wasmtime as a library:
|
|
|
|
Embedding wasmtime for a Plugin System
A more realistic embedding: a plugin host that loads multiple plugin components, each sandboxed:
|
|
This pattern is exactly what production plugin systems need: untrusted code, bounded resources, no capability leakage, fully typed interfaces.
Embedding wasmtime in Python
|
|
Fermyon Spin
Spin by Fermyon is the highest-level framework for building Component Model applications. It wraps the machinery of components, WASI, and wasmtime into a developer experience focused on building HTTP microservices and event-driven applications.
Installation and Setup
|
|
spin.toml Deep Dive
The spin.toml manifest is the center of a Spin application:
|
|
The power of the allowed_outbound_hosts list is architectural: it makes the network capabilities of each component explicit in the manifest, not buried in code. You can audit the entire blast radius of a compromised component from a single file.
Spin HTTP Handler in Depth
|
|
Spin Triggers: Beyond HTTP
|
|
|
|
Deploying Spin Applications
|
|
Composing Components
Component composition is the mechanism for linking components together into a larger system at the WASM level — before any runtime is involved. This is fundamentally different from service composition (where services communicate over a network) or library linking (where they share memory). Composed components maintain isolation but call each other through the Canonical ABI with no network overhead.
The Composition Model
When you compose components, you are creating a new component that contains both (or all) of the input components. The imports of one component are satisfied by the exports of another. The result is a single .wasm file that is self-contained.
┌─────────────────────────────────────────────────────────────────┐
│ Composed Component │
│ │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ HTTP Handler │ │ Database Component │ │
│ │ (Rust) │ │ (Go) │ │
│ │ │ │ │ │
│ │ imports: │───▶│ exports: │ │
│ │ myapp:db/store │ │ myapp:db/store │ │
│ │ │ │ │ │
│ │ exports: │ │ imports: │ │
│ │ wasi:http/... │ │ wasi:filesystem/... │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
│ │
│ Unsatisfied imports (from outer world): │
│ wasi:http/incoming-handler │
│ wasi:filesystem/preopens │
└─────────────────────────────────────────────────────────────────┘
Using wasm-tools compose
|
|
A Practical Composition Example
Suppose you have three components:
- http-handler (Rust): Handles HTTP requests, needs a key-value store and a logger
- kv-store (Go): Implements the key-value store interface, backed by in-memory storage
- structured-logger (TypeScript): Implements the logger interface, outputs JSON
Their WIT interfaces:
// wit/kv.wit — the key-value interface
package myapp:kv@0.1.0;
interface store {
get: func(key: string) -> option<list<u8>>;
set: func(key: string, value: list<u8>) -> result<_, string>;
delete: func(key: string) -> result<bool, string>;
list-keys: func(prefix: option<string>) -> list<string>;
}
// wit/logging.wit
package myapp:logging@0.1.0;
interface logger {
enum level { trace, debug, info, warn, error }
log: func(level: level, component: string, message: string, fields: list<tuple<string, string>>) -> _;
}
// wit/http-handler-world.wit
package myapp:http-handler@0.1.0;
world http-handler {
import myapp:kv/store@0.1.0;
import myapp:logging/logger@0.1.0;
export wasi:http/incoming-handler@0.2.0;
}
Build each component separately:
|
|
Compose them:
|
|
Selective Composition with a Configuration File
For complex compositions, wasm-tools compose supports a YAML configuration file:
|
|
|
|
Dynamic Composition at Runtime
wasm-tools compose produces static compositions baked into the binary. For dynamic composition — loading different implementations at runtime — you use the wasmtime embedding API with dynamic linking:
|
|
Plugin Systems with the Component Model
Plugin systems are arguably the most compelling near-term use case for the Component Model. The requirements for a plugin system are almost perfectly matched by what components provide:
| Plugin System Requirement | Component Model Feature |
|---|---|
| Plugins in any language | WIT bindings for Rust, Go, Python, JS, C |
| Sandboxed execution | Shared-nothing isolation + capability grants |
| Typed interfaces | WIT interface definitions |
| Version compatibility | WIT package versioning |
| Runtime loading | Component binary format + wasmtime API |
| Resource limits | wasmtime Store fuel + memory limits |
| Hot reloading | Fresh Store per invocation |
Designing a Plugin Interface in WIT
// wit/plugin-api.wit
package myapp:plugin-api@1.0.0;
// What plugins can call from the host
interface host-capabilities {
// Logging (sandboxed — plugins can't write to disk directly)
log-info: func(message: string) -> _;
log-warn: func(message: string) -> _;
log-error: func(message: string) -> _;
// Key-value storage (scoped to the plugin's namespace)
kv-get: func(key: string) -> option<list<u8>>;
kv-set: func(key: string, value: list<u8>) -> result<_, string>;
// Outbound HTTP (controlled by host policy)
http-fetch: func(url: string, headers: list<tuple<string, string>>) -> result<http-response, string>;
record http-response {
status: u16,
headers: list<tuple<string, string>>,
body: list<u8>,
}
}
// What plugins must implement (exports to the host)
interface plugin-lifecycle {
// Called once when the plugin is loaded
on-load: func(config: list<tuple<string, string>>) -> result<_, string>;
// Plugin metadata
name: func() -> string;
version: func() -> string;
description: func() -> string;
}
// The event-processing interface (what makes it a processing plugin)
interface event-processor {
record event {
id: string,
event-type: string,
source: string,
timestamp: u64,
payload: list<u8>,
}
record processing-result {
success: bool,
output-events: list<event>,
error-message: option<string>,
metadata: list<tuple<string, string>>,
}
// Process an event and optionally emit new events
process: func(event: event) -> processing-result;
}
// The world that plugin components implement
world event-processing-plugin {
import host-capabilities;
export plugin-lifecycle;
export event-processor;
}
// The world that the host application implements (inverse perspective)
world plugin-host {
export host-capabilities;
import plugin-lifecycle;
import event-processor;
}
Building a Plugin in Rust
|
|
Real-World Plugin Systems Using Components
Zed Editor: Zed (the high-performance code editor written in Rust) uses WebAssembly for its extension system. Extensions are .wasm components that implement Zed’s WIT interfaces. This lets any language with Wasm support write Zed extensions — the same binary works on macOS, Linux, and Windows without recompilation.
Extism: The Extism project provides a higher-level plugin framework built on the Component Model concepts. It supports Rust, Go, Python, JavaScript, Ruby, PHP, and more as both host languages and plugin languages.
|
|
WASM Filters in Envoy/Istio: Envoy proxy supports WASM plugins for request/response processing. These are not full Component Model yet (they use an older WASM ABI), but they demonstrate the pattern at massive scale — these run on every request at the mesh layer.
The Polyglot Story
One of the headline promises of the Component Model is true language interoperability: a Python component calling a Rust component calling a Go component, all in the same process, all communicating through typed WIT interfaces.
A Polyglot Pipeline Example
Suppose you are building a document processing pipeline:
- Rust component: HTTP ingestion, validation, routing (fastest for networking)
- Python component: ML-based document classification (Python ML ecosystem)
- Go component: Structured data extraction and storage (Go for ergonomics)
The WIT interfaces:
// wit/pipeline.wit
package myorg:docpipeline@0.1.0;
interface ingester {
record raw-document {
id: string,
content-type: string,
data: list<u8>,
metadata: list<tuple<string, string>>,
}
ingest: func(doc: raw-document) -> result<string, string>; // returns doc-id
}
interface classifier {
enum document-class {
invoice,
contract,
report,
email,
unknown,
}
record classification-result {
doc-id: string,
class: document-class,
confidence: f32,
labels: list<tuple<string, f32>>,
}
classify: func(doc-id: string, content: list<u8>) -> result<classification-result, string>;
}
interface extractor {
record extracted-data {
doc-id: string,
fields: list<tuple<string, string>>,
tables: list<list<list<string>>>,
errors: list<string>,
}
extract: func(doc-id: string, doc-class: string, content: list<u8>) -> result<extracted-data, string>;
store: func(data: extracted-data) -> result<_, string>;
}
world pipeline-orchestrator {
import classifier;
import extractor;
import wasi:http/types@0.2.0;
export ingester;
export wasi:http/incoming-handler@0.2.0;
}
world classifier-component {
export classifier;
}
world extractor-component {
export extractor;
import wasi:filesystem/preopens@0.2.0;
}
The Rust orchestrator that calls both:
|
|
The Python classifier:
|
|
The Go extractor:
|
|
Compose all three:
|
|
Note on Python embedding: componentize-py embeds a minimal Python interpreter inside the WASM component, compiled to WASM32. This makes Python components significantly larger than Rust or Go (typically 5-20MB vs 100KB-2MB), but the isolation and interface benefits remain.
WASI Preview 2 Interfaces
WASI Preview 2 is the collection of standard WIT interfaces maintained by the WASI subgroup of the WebAssembly CG. It shipped as stable in January 2024.
Core Stable Interfaces
wasi:io — The foundational streaming I/O interface. Everything else in WASI that does I/O builds on these types.
// wasi:io/streams
interface streams {
resource input-stream {
read: func(len: u64) -> result<list<u8>, stream-error>;
blocking-read: func(len: u64) -> result<list<u8>, stream-error>;
subscribe: func() -> pollable;
}
resource output-stream {
write: func(contents: list<u8>) -> result<_, stream-error>;
blocking-write-and-flush: func(contents: list<u8>) -> result<_, stream-error>;
flush: func() -> result<_, stream-error>;
subscribe: func() -> pollable;
}
variant stream-error { last-operation-failed(error), closed }
}
// wasi:io/poll
interface poll {
resource pollable {
block: func() -> _;
ready: func() -> bool;
}
poll: func(in: list<borrow<pollable>>) -> list<u32>;
}
wasi:clocks — Wall clock and monotonic clock access.
interface wall-clock {
record datetime {
seconds: u64,
nanoseconds: u32,
}
now: func() -> datetime;
resolution: func() -> datetime;
}
interface monotonic-clock {
type instant = u64; // nanoseconds
type duration = u64;
now: func() -> instant;
resolution: func() -> duration;
subscribe-instant: func(when: instant) -> pollable;
subscribe-duration: func(when: duration) -> pollable;
}
wasi:filesystem — POSIX-like filesystem access, capability-based.
interface types {
resource descriptor {
// File operations
read-via-stream: func(offset: filesize) -> result<input-stream, error-code>;
write-via-stream: func(offset: filesize) -> result<output-stream, error-code>;
append-via-stream: func() -> result<output-stream, error-code>;
stat: func() -> result<descriptor-stat, error-code>;
set-size: func(size: filesize) -> result<_, error-code>;
// Directory operations
open-at: func(
path-flags: path-flags,
path: string,
open-flags: open-flags,
flags: descriptor-flags,
) -> result<descriptor, error-code>;
read-directory: func() -> result<directory-entry-stream, error-code>;
unlink-file-at: func(path: string) -> result<_, error-code>;
rename-at: func(old-path: string, new-descriptor: borrow<descriptor>, new-path: string) -> result<_, error-code>;
}
}
interface preopens {
// Returns the pre-opened directory handles granted by the host
get-directories: func() -> list<tuple<descriptor, string>>;
}
wasi:random — Cryptographically secure random number generation.
interface random {
get-random-bytes: func(len: u64) -> list<u8>;
get-random-u64: func() -> u64;
}
interface insecure {
get-insecure-random-bytes: func(len: u64) -> list<u8>;
get-insecure-random-u64: func() -> u64;
}
wasi:cli — Standard streams, environment, arguments, and exit.
interface environment {
get-environment: func() -> list<tuple<string, string>>;
get-arguments: func() -> list<string>;
initial-cwd: func() -> option<string>;
}
interface stdin {
get-stdin: func() -> input-stream;
}
interface stdout {
get-stdout: func() -> output-stream;
}
interface run {
run: func() -> result;
}
wasi:sockets — TCP and UDP networking. Landed in Preview 2.
// wasi:sockets/tcp
interface tcp {
resource tcp-socket {
start-bind: func(network: borrow<network>, local-address: ip-socket-address) -> result<_, error-code>;
finish-bind: func() -> result<_, error-code>;
start-connect: func(network: borrow<network>, remote-address: ip-socket-address) -> result<_, error-code>;
finish-connect: func() -> result<tuple<input-stream, output-stream>, error-code>;
start-listen: func() -> result<_, error-code>;
finish-listen: func() -> result<_, error-code>;
accept: func() -> result<tuple<tcp-socket, input-stream, output-stream>, error-code>;
local-address: func() -> result<ip-socket-address, error-code>;
remote-address: func() -> result<ip-socket-address, error-code>;
// ... more methods
}
create-tcp-socket: func(address-family: ip-address-family) -> result<tcp-socket, error-code>;
}
wasi:http — HTTP client and server. This is what makes Spin and wasmtime serve work.
// wasi:http/types
interface types {
resource incoming-request {
method: func() -> method;
path-with-query: func() -> option<string>;
scheme: func() -> option<scheme>;
authority: func() -> option<string>;
headers: func() -> headers;
consume: func() -> result<incoming-body>;
}
resource outgoing-request {
constructor(headers: headers);
body: func() -> result<outgoing-body>;
method: func() -> method;
set-method: func(method: method) -> result;
path-with-query: func() -> option<string>;
set-path-with-query: func(path-with-query: option<string>) -> result;
authority: func() -> option<string>;
set-authority: func(authority: option<string>) -> result;
scheme: func() -> option<scheme>;
set-scheme: func(scheme: option<scheme>) -> result;
}
resource outgoing-response {
constructor(headers: headers);
status-code: func() -> status-code;
set-status-code: func(status-code: status-code) -> result;
body: func() -> result<outgoing-body>;
}
resource incoming-body {
stream: func() -> result<input-stream>;
%finish: static func(this: incoming-body) -> future-trailers;
}
}
// wasi:http/incoming-handler — what HTTP server components implement
interface incoming-handler {
handle: func(request: incoming-request, response-out: response-outparam) -> _;
}
// wasi:http/outgoing-handler — what HTTP client components import
interface outgoing-handler {
handle: func(
request: outgoing-request,
options: option<request-options>,
) -> result<future-incoming-response, error-code>;
}
Interfaces in Progress (WASI 0.3 / Preview 3)
Several important interfaces are still being finalized:
wasi:messaging — Pub/sub messaging (Kafka, NATS, etc.). The draft exists but runtime support is limited.
wasi:sql — Relational database access. Currently most frameworks use their own proprietary interfaces (Spin has fermyon:spin/sqlite).
wasi:blobstore — Object storage (S3-compatible). Draft exists.
Async/Streams in Preview 3: WASI Preview 3 introduces native async support using the future<T> and stream<T> types in WIT. This is significant — Preview 2 handles async through polling and non-blocking APIs, which is workable but verbose. Preview 3’s async model will enable cleaner async code patterns across all host languages.
// Preview 3 async WIT (draft)
interface async-processor {
// stream<T> allows incremental production/consumption
process-stream: func(input: stream<u8>) -> stream<result<u8, string>>;
// future<T> for single async values
fetch-data: func(url: string) -> future<result<list<u8>, string>>;
}
Current State and Honest Assessment
What Is Production-Ready
Spin on Fermyon Cloud: Completely production-ready. Fermyon runs a managed cloud platform for Spin applications. It handles HTTP triggers, key-value storage, SQLite, and Redis with high availability. The Spin v3.x SDK is stable. This is the best path for teams who want to ship Component Model applications today without managing infrastructure.
Rust components: First-class support. cargo-component, wit-bindgen, and the Rust WASM target are mature. The tooling is fast and the debugging experience (while not perfect) is reasonable with DWARF support in wasmtime.
Basic component composition: wasm-tools compose works reliably for statically composing components. If you control both components and they share a compatible WIT interface, composition is straightforward.
wasmtime as a library: Embedding wasmtime for plugin systems is production-quality. Companies like Fastly (which runs billions of requests through Wasm) have used wasmtime in production for years. The Component Model embedding API is stable as of wasmtime 18+.
WASI Preview 2 core interfaces: wasi:io, wasi:clocks, wasi:random, wasi:filesystem, wasi:cli are stable and implemented consistently across wasmtime and Spin. wasi:http is stable and the primary interface for server-side components.
SpinKube: Running Spin applications on Kubernetes via SpinKube is production-ready. It is in active use at organizations running Wasm workloads at scale.
What Is Still Maturing
Python components: componentize-py works but produces large binaries (typically 10-30MB because the Python interpreter is embedded in the WASM). Startup is slower than Rust. Import compatibility is incomplete — you cannot use all Python packages, only those with pure-Python or WASM-compatible implementations. This will improve as the Python ecosystem adapts, but it is not seamless today.
Go components: TinyGo support for WASM components is functional but the Go standard library coverage is incomplete. Full go (not TinyGo) support for wasm32-wasip2 is in progress. For simple use cases TinyGo is fine, but complex Go programs may hit missing standard library features.
JavaScript/TypeScript via jco: Workable for server-side use cases. The jco transpilation to Node.js modules works. Building components from JS is reasonable for simple cases. Performance is V8-in-WASM, which is slower than native.
Component debugging: Debugging Wasm components is inferior to native debugging. You can get source-level stack traces in wasmtime with DWARF info, but interactive debugging (breakpoints, variable inspection in a GUI debugger) is limited. The tooling is actively improving — the Bytecode Alliance has invested in debug tooling — but it is not at the level of native development.
Full WASI sockets coverage: wasi:sockets is in Preview 2 and wasmtime supports it, but not all runtimes do. If you need raw TCP/UDP socket access in your components (not just the HTTP interface), verify your target runtime’s support.
The toolchain fragmentation story: You will encounter different versions of WIT world names, different crate versions of wit-bindgen, different WASI feature flags needed. The ecosystem is moving fast and version compatibility requires attention. Pinning exact toolchain versions in your CI is important.
Component Model vs Docker: Where Wasm Wins
For general-purpose server workloads — databases, complex stateful services, applications with extensive library dependencies — containers remain the right tool. Docker’s ecosystem, debugging tools, and operational familiarity are massive advantages.
The Component Model beats Docker for specific categories:
Plugin systems: If you need to run untrusted code from external sources (user-defined logic, third-party extensions, user-uploaded transformations), components are strictly better. Docker containers as plugins are operationally heavy and share the kernel. WASM components are microsecond-startup sandboxes with typed interfaces. There is no comparison.
Edge functions: Latency-sensitive request processing at CDN/edge nodes. Docker containers at the edge are impractical — you cannot cold-start a container in 1ms. WASM components can. Cloudflare Workers and Fastly Compute prove this at internet scale.
Polyglot microservices with tight coupling: If you have services that need to call each other in a tight loop with minimal serialization overhead, component composition is more efficient than gRPC or HTTP between containers. The Canonical ABI overhead for crossing component boundaries is measured in nanoseconds, not milliseconds.
Untrusted code execution platforms: If you are building a platform where customers run their own code (like serverless functions, data transformation pipelines, or customizable business logic), the WASM sandbox with explicit capability grants gives you much stronger isolation guarantees than containers with a much lower overhead. The market for this is growing.
Highly resource-constrained environments: IoT and embedded systems where 10MB containers are too large. A WASM component for a simple HTTP handler can be 200KB.
The Roadmap
The Bytecode Alliance’s near-term priorities:
WASI 0.3 / Preview 3: Native async support with future<T> and stream<T> types. This will significantly improve the ergonomics of writing async components and close the gap with native async programming models.
Component Model Registries: A standard OCI-compatible registry for distributing WIT packages and component binaries. The warg (WebAssembly Registry) protocol is in development.
Threads: wasi-threads is experimental but moving toward standardization. This will allow components to do parallel CPU work within a single component without spawning multiple instances.
Better language tooling: The Go, Python, and JS toolchains are actively improving with each release. The 12-month trajectory for these has been significant — compare componentize-py today to its state 18 months ago.
Getting Started: A Practical Path
If you want to get hands-on with the Component Model today:
|
|
The broader resource list:
github.com/bytecodealliance/component-docs— official Component Model docsgithub.com/WebAssembly/component-model— the specification itselfgithub.com/WebAssembly/WASI— WASI interfaces and proposalsgithub.com/bytecodealliance/wit-bindgen— binding generatorsgithub.com/bytecodealliance/wasm-tools— toolchaindeveloper.fermyon.com— Spin docs and tutorialsdocs.wasmtime.dev— wasmtime embedding guide
The Component Model is not yet frictionless. You will hit toolchain version mismatches, incomplete language support, and sparse documentation for edge cases. But the foundation is sound, the specification is stable, and the tooling is improving rapidly. Engineers who understand the Component Model today are building expertise that will be genuinely valuable as WASM moves from emerging to mainstream. The “WebAssembly is only for browsers” era is definitively over.
Comments