Skip to main content

WebAssembly Serverless Architecture 2026: WASI Preview 2, Component Model, and Edge Deployment

Published: March 16, 2026 Updated: May 24, 2026 Larry Qu 10 min read

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

Comments

👍 Was this article helpful?