Introduction
WebAssembly (Wasm) has evolved from a browser-only technology into a universal runtime for serverless computing. The key developments in 2026 — WASI Preview 2 reaching production stability, the Component Model enabling cross-language module composition, and Wasm runtimes achieving sub-millisecond cold starts — have made Wasm a practical alternative to containers for edge functions, microservices, and AI inference at the edge.
This guide covers the WASI system interface, the Component Model for polyglot programming, the major serverless frameworks (Spin 3.0, wasmCloud), Rust code examples for building Wasm HTTP services, and deployment patterns for edge and cloud environments.
WASI: The System Interface for Wasm
WASI (WebAssembly System Interface) provides standardized APIs for Wasm modules to interact with the operating system — files, network sockets, HTTP, clocks, random numbers. WASI Preview 2 reached production stability in 2025-2026, and WASI Preview 3 (0.3.0, February 2026) added native async I/O support with a futures-and-streams model.
flowchart LR
subgraph WasmModule["Wasm Component<br/>(Rust, Go, C++)"]
A[Application Code]
end
subgraph WASI["WASI Preview 2 APIs"]
FS[wasi:filesystem<br/>File I/O]
SOCK[wasi:sockets<br/>TCP/UDP]
HTTP[wasi:http<br/>HTTP requests]
CLI[wasi:cli<br/>Args, env, stdio]
RAND[wasi:random<br/>Secure random]
CLOCK[wasi:clocks<br/>Monotonic + wall clock]
end
subgraph Runtime["Wasm Runtime<br/>Wasmtime / WasmEdge / Wasmer"]
RT[Execution Engine]
end
subgraph Host["Host OS"]
OS[Linux / Windows / macOS Kernel]
end
A <--> WASI
WASI <--> RT
RT <--> OS
WASI Preview 2 provides capability-based security — each API must be explicitly granted to the module. A module that only needs HTTP cannot access the filesystem unless wasi:filesystem is in its allowlist. This is a stronger security model than containers, where any process in the container can access all container resources.
WASI Preview 2 vs Preview 3
| Feature | Preview 2 (stable) | Preview 3 (0.3.0, Feb 2026) |
|---|---|---|
| I/O Model | Blocking | Async (futures + streams) |
| HTTP | Client + Server | Client + Server (improved) |
| Networking | TCP, UDP | TCP, UDP, QUIC |
| Component Model | Yes | Yes (enhanced) |
| Production ready | Yes | Early adopter |
| Runtime support | Wasmtime 22+ | Wasmtime 44+ |
Component Model: Polyglot Programming
The Component Model lets Wasm modules written in different languages (Rust, Go, Python, C++) interoperate seamlessly through a shared type system. WIT (Wasm Interface Types) defines the interfaces, and wit-bindgen generates language-specific bindings.
WIT Interface Definition
// hello.wit — define a component interface
package example:hello;
interface hello-world {
greet: func(name: string) -> string;
}
world hello-world-server {
import hello-world;
export wasi:http/handler;
}
Rust HTTP Component (using WASI Preview 2)
// Cargo.toml
// [dependencies]
// wasi = "0.13"
// wasi-http = "0.4"
use wasi::http::incoming_handler;
use wasi::http::types::{
IncomingRequest, ResponseOutparam, OutgoingResponse,
OutgoingBody, Headers, Method,
};
struct MyServer;
impl incoming_handler::Guest for MyServer {
fn handle(request: IncomingRequest, response_out: ResponseOutparam) {
let path = request.path_with_query().unwrap_or_default();
let method = request.method();
let (status, body_text) = match (method, path.as_str()) {
(Method::Get, "/") => (200, "Hello from Wasm!".to_string()),
(Method::Get, "/health") => {
(200, r#"{"status": "healthy", "version": "1.0.0"}"#.into())
}
(Method::Post, "/echo") => {
// Read request body (blocking read up to 1MB)
let body = request.consume().unwrap();
let stream = body.stream().unwrap();
let data = stream.blocking_read(1024 * 1024).unwrap();
let text = String::from_utf8_lossy(&data).to_string();
(200, text)
}
_ => (404, "Not Found".to_string()),
};
// Build the response
let headers = Headers::new();
let response = OutgoingResponse::new(status, &headers);
let body = response.body().unwrap();
body.stream().unwrap().blocking_write(body_text.as_bytes()).unwrap();
body.finish().unwrap();
ResponseOutparam::set(response_out, Ok(response));
}
}
// Required for the component model entry point
wasi::http::incoming_handler::export!(MyServer);
Compile with:
cargo build --target wasm32-wasip2 --release
The output .wasm file can run on any WASI Preview 2-compatible runtime: Wasmtime v44+, Spin 3.0+, or wasmCloud.
Serverless Frameworks
Wasmtime v44.0.0 (April 2026)
The reference Wasm runtime with full WASI Preview 2 support:
# Install Wasmtime
curl -fsSL https://wasmtime.dev/install.sh | bash
# Run a Wasm HTTP component
wasmtime serve my-component.wasm --addr 0.0.0.0:8080
# Test the endpoint
curl http://localhost:8080/health
# {"status": "healthy", "version": "1.0.0"}
Fermyon Spin 3.0 (GA, 2026)
Spin is a framework for building event-driven microservices with Wasm components. Version 3.0 uses the Component Model natively:
# Install Spin
curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash
# Create a new Spin application
spin new my-app --template http-rust
cd my-app
# Build and deploy
spin build
spin deploy # to Fermyon Cloud or self-hosted
# spin.toml — Spin 3.0 application manifest
spin_manifest_version = "3"
name = "api-gateway"
version = "1.0.0"
[[trigger.http]]
route = "/api/..."
component = "router"
[component.router]
source = "target/wasm32-wasip2/release/router.wasm"
# WASI capabilities are declared explicitly
[component.router.wasi]
filesystem = { directories = ["/data"] }
http = true
wasmCloud (CNCF Incubation)
wasmCloud provides an actor-based platform for distributed Wasm applications with capability-based security:
# Start a wasmCloud host
wasmcloud_host
# Deploy a component via wash CLI
wash app deploy my-app.wadm.yaml
# wadm.yaml — wasmCloud application definition
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: edge-gateway
spec:
components:
- name: http-handler
type: actor
properties:
image: ghcr.io/myorg/http-handler:v1
traits:
- type: spreadscaler
properties:
spread: [{requirements: {"zone": "us-east"}, weight: 1}]
- type: link
properties:
target: keyvalue-store
namespace: wasi
package: keyvalue
Performance Comparison
| Metric | Container (Docker) | Wasm (Wasmtime) | AWS Lambda (container) |
|---|---|---|---|
| Cold start | 100ms - 5s | 1-5ms | 100ms - 2s |
| Warm start | 1-5ms | 1-50µs | 1-5ms |
| Memory overhead | 50-500MB | 1-10MB | 50-100MB |
| Binary size | 10-100MB+ | 0.1-5MB | 10-100MB |
| Throughput (HTTP) | ~15K req/s | ~50K req/s | ~10K req/s |
Wasm’s sub-millisecond cold start eliminates the need for warm pools in serverless deployments. Combined with 10-50x smaller memory footprint, a single host can run thousands of Wasm components where it could run tens of containers.
Edge Deployment Pattern
flowchart TD
CDN[CDN / Edge Gateway] -->|Route by path| R{Router}
R -->|/api/auth| Auth[Auth Component<br/>Rust, 0.5ms startup]
R -->|/api/users| Users[User API Component<br/>Go, 0.3ms startup]
R -->|/api/search| Search[Search Component<br/>Python, 1.2ms startup]
Auth --> KV[(Key-Value Store<br/>Redis)]
Users --> PG[(PostgreSQL)]
Search --> ES[(Elasticsearch)]
Auth -->|WASI HTTP| Users
Users -->|Component link| Search
Each component is an independent Wasm module with explicit capability declarations. The edge router starts components on demand in 1-5ms and caches them for subsequent requests. Components can call each other through WASI HTTP or component model linking.
Observability and Tracing for Wasm Serverless
OpenTelemetry Integration
Wasm components can export traces and metrics through WASI logging interfaces:
use opentelemetry::{
global,
trace::{Tracer, Span, SpanContext, TraceFlags},
KeyValue,
};
use opentelemetry_otlp::WithExportConfig;
fn init_observability() {
let tracer_provider = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint("http://otel-collector:4317")
)
.install_batch()
.expect("Failed to install OTLP tracer");
global::set_tracer_provider(tracer_provider);
}
async fn handle_request(req: IncomingRequest, response_out: ResponseOutparam) {
let tracer = global::tracer("http-handler");
let mut span = tracer.start("handle_request");
span.set_attribute(KeyValue::new("http.method", format!("{:?}", req.method())));
span.set_attribute(KeyValue::new("http.path", req.path_with_query().unwrap_or_default()));
// Process request with tracing context
let result = process_with_tracing(&tracer, &span, req).await;
span.set_attribute(KeyValue::new("http.status_code", result.status as i64));
// Emit custom metrics via WASI logging
wasi::logging::log(
wasi::logging::Level::Info,
"http-handler",
&format!(r#"{{"metric":"request_duration","value":{},"path":"{}"}}"#,
result.duration.as_millis(),
req.path_with_query().unwrap_or_default())
);
span.end();
}
Structured Logging
use serde_json::json;
use wasi::logging::logging;
#[derive(Serialize)]
struct StructuredLog {
timestamp: String,
level: String,
component: String,
message: String,
trace_id: Option<String>,
span_id: Option<String>,
attributes: serde_json::Value,
}
fn emit_log(level: Level, msg: &str, attrs: serde_json::Value) {
let log_entry = StructuredLog {
timestamp: chrono::Utc::now().to_rfc3339(),
level: format!("{:?}", level),
component: "payment-worker".into(),
message: msg.into(),
trace_id: get_current_trace_id(),
span_id: get_current_span_id(),
attributes: attrs,
};
let level = match level {
Level::Info => wasi::logging::Level::Info,
Level::Warn => wasi::logging::Level::Warn,
Level::Error => wasi::logging::Level::Error,
_ => wasi::logging::Level::Info,
};
wasi::logging::log(level, "payment-worker",
&serde_json::to_string(&log_entry).unwrap());
}
Metrics Export
use prometheus::{
Counter, Histogram, HistogramOpts, IntCounter, Registry,
opts, register_counter, register_histogram,
};
lazy_static! {
static ref HTTP_REQUESTS: IntCounter = register_int_counter!(
"http_requests_total",
"Total number of HTTP requests"
).unwrap();
static ref REQUEST_DURATION: Histogram = register_histogram!(
HistogramOpts::new("http_request_duration_ms", "Request duration in milliseconds")
.buckets(vec![1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0])
).unwrap();
}
fn record_metrics(status: u16, duration_ms: f64) {
HTTP_REQUESTS.inc();
REQUEST_DURATION.observe(duration_ms);
// Export metrics via WASI interface or HTTP push
let metrics = prometheus::gather();
let encoded = prometheus::TextEncoder::new()
.encode_to_string(&metrics)
.unwrap();
// Push to Prometheus Pushgateway
let client = reqwest::blocking::Client::new();
client.post("http://pushgateway:9091/metrics/job/wasm-component")
.body(encoded)
.send()
.ok();
}
Multi-Language Component Interaction
The Component Model enables calling between modules written in different languages:
Go Component (called from Rust)
// go-component/main.go
package main
import (
"github.com/bytecodealliance/wasm-tools-go/cmd"
)
//go:generate wit-bindgen-wrpc go --world processor --out-dir gen
func init() {
cmd.Processor(func(input string) (string, error) {
// Process the input (called by Rust component)
if input == "" {
return "", fmt.Errorf("empty input")
}
return strings.ToUpper(input), nil
})
}
func main() {}
Rust Component (calls Go)
// rust-component/src/lib.rs
use wasi::http::types::{IncomingRequest, ResponseOutparam};
use bindings::exports::example::processor::Processor;
wit_bindgen::generate!({
world: "orchestrator",
// Import the Go component's interface
imports: {
"example:processor/process": fn process(input: String) -> Result<String, String>,
},
});
struct Orchestrator;
impl Guest for Orchestrator {
fn handle_request(req: IncomingRequest, response_out: ResponseOutparam) {
// Get data from request
let body = read_body(&req);
// Call Go component for processing
match process(body) {
Ok(result) => send_response(response_out, 200, &result),
Err(e) => send_response(response_out, 500, &e),
}
}
}
State Management Patterns
Wasm components are stateless by design but need access to state:
In-Memory Caching
use std::collections::HashMap;
use std::sync::Mutex;
lazy_static! {
static ref CACHE: Mutex<HashMap<String, CacheEntry>> = Mutex::new(HashMap::new());
}
struct CacheEntry {
data: Vec<u8>,
expires_at: u64,
}
fn get_cached(key: &str) -> Option<Vec<u8>> {
let cache = CACHE.lock().unwrap();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
cache.get(key).and_then(|entry| {
if now < entry.expires_at {
Some(entry.data.clone())
} else {
None
}
})
}
External KV Store via WASI
use wasi::keyvalue::store;
fn get_user_preferences(user_id: &str) -> Result<UserPrefs, Error> {
let bucket = store::open("preferences")?;
let data = bucket.get(format!("user:{}", user_id))?
.ok_or(Error::NotFound)?;
let prefs: UserPrefs = serde_json::from_slice(&data)?;
Ok(prefs)
}
fn set_user_preferences(user_id: &str, prefs: &UserPrefs) -> Result<(), Error> {
let bucket = store::open("preferences")?;
let data = serde_json::to_vec(prefs)?;
bucket.set(format!("user:{}", user_id), &data)?;
Ok(())
}
Security Model Deep Dive
Wasm’s capability-based security is stricter than containers:
// A module can only access explicitly granted capabilities
// spin.toml manifest declares required WASI capabilities
// This module only has HTTP access — no filesystem, no network except HTTP
// If it tries to access the filesystem, the runtime rejects the call
fn handle(req: IncomingRequest, out: ResponseOutparam) {
// ✅ Allowed: WASI HTTP was granted
let client = wasi::http::Client::new();
let resp = client.get("https://api.example.com/data").unwrap();
// ❌ Blocked: Wasmtime will trap — no filesystem capability
// let file = std::fs::read_to_string("/etc/passwd").unwrap();
// ❌ Blocked: No raw socket capability
// let socket = std::net::TcpStream::connect("10.0.0.1:22").unwrap();
// ✅ Allowed: Random is explicitly granted
let key = wasi::random::random::get_random_u64();
send_response(out, 200, &resp.body);
}
Security Comparison
| Aspect | Container | Wasm Component |
|---|---|---|
| System calls | All (unless seccomp filtered) | Only granted WASI APIs |
| File access | Full filesystem (if mounted) | Only granted directories |
| Network | Full network stack | Only granted protocols |
| Memory isolation | Kernel-level | Language-level (sandboxed) |
| Side-channel resistance | Weak | Strong (deterministic execution) |
| Supply chain | Binary compatibility risks | Deterministic builds |
| Cold start privilege | Root → user | Sandboxed from first instruction |
Production Deployment Architecture
API Gateway + Wasm Router Pattern
# Spin 3.0 production manifest with multiple components
spin_manifest_version = "3"
name = "production-api"
version = "2.1.0"
[[trigger.http]]
route = "/api/v1/users"
component = "users-rust"
[[trigger.http]]
route = "/api/v1/search"
component = "search-python"
[[trigger.http]]
route = "/api/v1/reports"
component = "reports-go"
[[trigger.http]]
route = "/api/v1/health"
component = "health-check"
[component.users-rust]
source = "target/wasm32-wasip2/release/users.wasm"
[component.users-rust.wasi]
http = true
keyvalue = true
# Max instances for this component
max_instances = 100
[component.search-python]
source = "search/search.wasm"
[component.search-python.wasi]
http = true
# Python requires more memory
memory = "64MB"
max_instances = 20
[component.health-check]
source = "health/health.wasm"
[component.health-check.wasi]
http = true
http_outbound = ["https://status.example.com"]
Horizontal Autoscaling with Wasm
# wasmCloud autoscaling configuration
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: autoscaled-gateway
spec:
policies:
- name: cpu-target
type: replicas
properties:
minReplicas: 2
maxReplicas: 100
cpuTargetAverageUtilization: 70
- name: latency-target
type: replicas
properties:
p99LatencyThreshold: 200ms
scaleUpBy: 2
scaleDownBy: 1
components:
- name: http-handler
type: actor
properties:
image: ghcr.io/myorg/http-handler:v1
traits:
- type: autoscaler
properties:
policies:
- cpu-target
- latency-target
Local Development Workflow
# 1. Scaffold a new component
spin new my-service --template http-rust
cd my-service
# 2. Develop with hot reload
spin watch
# 3. Test locally
curl http://localhost:3000/health
# 4. Build optimized production binary
spin build --release
# 5. Run performance test
wasmtime serve target/wasm32-wasip2/release/my-service.wasm \
--addr 0.0.0.0:8080 \
--max-concurrent-instances 1000
# 6. Profile with Wasmtime profiling
wasmtime serve --profile fuel \
target/wasm32-wasip2/release/my-service.wasm
# 7. Deploy to production
spin deploy -e prod
Cost Analysis: Wasm vs Containers vs Lambda
| Scenario | Container (100 req/s) | Wasm (100 req/s) | Lambda (100 req/s) |
|---|---|---|---|
| Compute (always-on) | $73/mo (3 x t3.medium) | $22/mo (2 x t3.nano) | N/A |
| Compute (on-demand) | N/A | N/A | $45/mo (100M invocations) |
| Memory overhead | 1.5GB (3 x 512MB) | 40MB (20 x 2MB) | 300MB (pooled) |
| Cold starts | 2-5s | 1-5ms | 100ms-2s |
| Cost per million req | $0.73 | $0.22 | $0.45 |
Wasm serverless is ~3x cheaper than containers for always-on workloads and ~2x cheaper than Lambda for on-demand workloads, with dramatically better cold start performance.
Resources
- WebAssembly Official Site — Spec, design docs
- WASI Preview 2 Specification — System interface definitions
- Wasmtime Runtime — v44.0.0 release notes
- Fermyon Spin 3.0 — Serverless Wasm framework
- wasmCloud Documentation — Actor-based distributed Wasm
- WATI Component Model — Cross-language module composition
- CNCF Wasm Landscape — Ecosystem project overview
Comments