Skip to main content
โšก Calmops

WebAssembly (Wasm) and WASI: Portable Code for Edge, Serverless, and Plugins

Introduction

WebAssembly started as a way to run C++ in browsers. In 2026, it’s a universal compute substrate for:

  • Edge functions that start in microseconds (vs seconds for containers)
  • Plugin systems that safely run untrusted third-party code
  • Serverless with near-zero cold starts
  • Portable binaries that run on any OS/CPU without recompilation

The key insight: Wasm + WASI gives you a sandboxed, portable binary that’s smaller than a container image and starts faster than a process.

Why Wasm for Server-Side?

Container cold start:  2-10 seconds (pull image, start runtime, init app)
Process cold start:    100-500ms (fork, exec, init)
Wasm cold start:       1-10ms (load module, JIT compile, execute)

Container size:        50MB-1GB
Wasm module size:      1-20MB (no OS, no runtime bundled)

Container isolation:   OS-level namespaces
Wasm isolation:        Capability-based sandbox (no syscalls without explicit grant)

WASI: System Access for Wasm

WASI (WebAssembly System Interface) gives Wasm modules controlled access to the host system:

Without WASI: Wasm can only do pure computation (no files, no network)
With WASI:    Wasm can access files, network, env vars โ€” but only what you grant
// Rust program compiled to Wasm with WASI
use std::fs;
use std::env;

fn main() {
    // These work in Wasm+WASI if the host grants access
    let args: Vec<String> = env::args().collect();
    let content = fs::read_to_string(&args[1]).unwrap();
    println!("File contents: {}", content);
}
# Compile Rust to Wasm
rustup target add wasm32-wasi
cargo build --target wasm32-wasi --release

# Run with Wasmtime (only grants access to /tmp)
wasmtime --dir /tmp target/wasm32-wasi/release/myapp.wasm /tmp/test.txt

# The module CANNOT access /etc/passwd even if it tries
# โ†’ capability-based security

Wasmtime: The Production Runtime

Wasmtime is the reference Wasm runtime from the Bytecode Alliance:

# Install
curl https://wasmtime.dev/install.sh -sSf | bash

# Run a Wasm module
wasmtime hello.wasm

# Grant filesystem access
wasmtime --dir /data myapp.wasm

# Grant network access (WASI preview2)
wasmtime --wasi-modules=experimental-wasi-sockets myapp.wasm

# Set environment variables
wasmtime --env KEY=value myapp.wasm

# Limit memory (in pages, 1 page = 64KB)
wasmtime --max-wasm-stack 1048576 myapp.wasm

Embedding Wasmtime in Go

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/bytecodealliance/wasmtime-go/v14"
)

func main() {
    // Load Wasm module
    wasmBytes, _ := os.ReadFile("plugin.wasm")

    engine := wasmtime.NewEngine()
    store  := wasmtime.NewStore(engine)
    module, _ := wasmtime.NewModule(engine, wasmBytes)

    // Create instance with WASI
    wasiConfig := wasmtime.NewWasiConfig()
    wasiConfig.InheritStdout()
    wasiConfig.InheritStderr()
    store.SetWasi(wasiConfig)

    linker := wasmtime.NewLinker(engine)
    linker.DefineWasi()

    instance, _ := linker.Instantiate(store, module)

    // Call exported function
    fn := instance.GetFunc(store, "process")
    result, _ := fn.Call(store, int32(42))
    fmt.Println("Result:", result)
}

Embedding Wasmtime in Python

from wasmtime import Store, Module, Instance, WasiConfig, Linker, Engine

def run_wasm_plugin(wasm_path: str, input_data: bytes) -> bytes:
    engine = Engine()
    store  = Store(engine)

    # Configure WASI โ€” no filesystem access, no network
    wasi = WasiConfig()
    wasi.inherit_stdout()
    store.set_wasi(wasi)

    module = Module.from_file(engine, wasm_path)
    linker = Linker(engine)
    linker.define_wasi()

    instance = linker.instantiate(store, module)

    # Call the plugin's process function
    process = instance.exports(store)["process"]
    memory  = instance.exports(store)["memory"]

    # Write input to Wasm memory
    ptr = instance.exports(store)["alloc"](store, len(input_data))
    memory.data_ptr(store)[ptr:ptr+len(input_data)] = input_data

    # Call process
    result_ptr = process(store, ptr, len(input_data))

    # Read result from Wasm memory
    # ... (read length-prefixed bytes from result_ptr)
    return result

Spin: Serverless Wasm Functions

Spin by Fermyon is the easiest way to build serverless Wasm functions:

# Install Spin
curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash

# Create a new Spin app
spin new http-rust my-api
cd my-api

# Build and run locally
spin build
spin up
# Listening on http://127.0.0.1:3000
// src/lib.rs โ€” Spin HTTP handler in Rust
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_component;

#[http_component]
fn handle_request(req: Request) -> anyhow::Result<impl IntoResponse> {
    let body = format!("Hello from Wasm! Path: {}", req.uri().path());

    Ok(Response::builder()
        .status(200)
        .header("content-type", "text/plain")
        .body(body)
        .build())
}
# spin.toml
spin_manifest_version = 2

[application]
name = "my-api"
version = "0.1.0"

[[trigger.http]]
route = "/..."
component = "my-api"

[component.my-api]
source = "target/wasm32-wasi/release/my_api.wasm"
allowed_outbound_hosts = ["https://api.example.com"]

[component.my-api.build]
command = "cargo build --target wasm32-wasi --release"

Spin with TypeScript

// src/index.ts
import { HandleRequest, HttpRequest, HttpResponse } from "@fermyon/spin-sdk"

export const handleRequest: HandleRequest = async (request: HttpRequest): Promise<HttpResponse> => {
    const name = new URL(request.uri).searchParams.get("name") ?? "World"

    return {
        status: 200,
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ message: `Hello, ${name}!` })
    }
}

Plugin Systems with Wasm Sandboxing

Wasm is ideal for running untrusted third-party plugins safely:

// plugin_host.go โ€” host application that runs Wasm plugins
package main

import (
    "context"
    "fmt"
    "time"

    "github.com/bytecodealliance/wasmtime-go/v14"
)

type PluginHost struct {
    engine *wasmtime.Engine
}

type PluginResult struct {
    Output []byte
    Error  error
}

func (h *PluginHost) RunPlugin(pluginPath string, input []byte, timeout time.Duration) PluginResult {
    store := wasmtime.NewStore(h.engine)

    // Resource limits โ€” prevent runaway plugins
    store.SetFuel(1_000_000)  // limit CPU instructions
    store.Limiter(
        1024*1024*10,  // 10MB max memory
        -1,            // unlimited tables
        -1,            // unlimited instances
        -1,            // unlimited memories
        -1,            // unlimited globals
    )

    // No filesystem, no network โ€” pure computation only
    wasi := wasmtime.NewWasiConfig()
    store.SetWasi(wasi)

    module, err := wasmtime.NewModuleFromFile(h.engine, pluginPath)
    if err != nil {
        return PluginResult{Error: fmt.Errorf("load plugin: %w", err)}
    }

    linker := wasmtime.NewLinker(h.engine)
    linker.DefineWasi()

    // Timeout via context
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    instance, err := linker.Instantiate(store, module)
    if err != nil {
        return PluginResult{Error: fmt.Errorf("instantiate: %w", err)}
    }

    // Call plugin's transform function
    transform := instance.GetFunc(store, "transform")
    if transform == nil {
        return PluginResult{Error: fmt.Errorf("plugin missing 'transform' export")}
    }

    // ... write input to memory, call function, read output
    return PluginResult{Output: []byte("result")}
}

// Usage: safely run untrusted plugins
func main() {
    host := &PluginHost{engine: wasmtime.NewEngine()}

    // Run user-provided plugin โ€” sandboxed, can't harm the host
    result := host.RunPlugin(
        "user-plugin.wasm",
        []byte(`{"data": "process this"}`),
        5*time.Second,
    )

    if result.Error != nil {
        fmt.Println("Plugin error:", result.Error)
    } else {
        fmt.Println("Plugin output:", string(result.Output))
    }
}

Wasm on Kubernetes with Krustlet / WASM Node

# Deploy a Wasm workload to Kubernetes
# (requires a Wasm-capable node runtime like containerd-wasm-shims)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wasm-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: wasm-app
  template:
    metadata:
      labels:
        app: wasm-app
    spec:
      # Schedule on Wasm-capable nodes
      nodeSelector:
        kubernetes.io/arch: wasm32
      runtimeClassName: wasmtime  # Wasm runtime class
      containers:
      - name: app
        image: ghcr.io/myorg/myapp:latest  # OCI image containing .wasm
        resources:
          limits:
            memory: "64Mi"  # Much smaller than container workloads
            cpu: "100m"

Language Support

Language Wasm Support WASI Support Notes
Rust Excellent Excellent Best ecosystem, smallest binaries
C/C++ Excellent Good Via Emscripten or wasi-sdk
Go Good Good GOOS=wasip1 GOARCH=wasm
Python Good Good Via Pyodide or py2wasm
TypeScript/JS Good Good Via Javy or QuickJS
Java Experimental Experimental Via TeaVM or GraalVM
# Compile Go to Wasm
GOOS=wasip1 GOARCH=wasm go build -o main.wasm .
wasmtime main.wasm

# Compile C to Wasm
/opt/wasi-sdk/bin/clang --sysroot=/opt/wasi-sdk/share/wasi-sysroot \
    -o hello.wasm hello.c
wasmtime hello.wasm

When to Use Wasm

โœ“ Edge functions needing <10ms cold start
โœ“ Plugin systems requiring sandboxed untrusted code
โœ“ Serverless with very high invocation rates (cold starts matter)
โœ“ Portable binaries across OS/CPU architectures
โœ“ Multi-tenant compute where isolation is critical

โœ— Long-running services (containers are better)
โœ— GPU workloads (limited GPU support)
โœ— Applications needing full POSIX compatibility
โœ— When your language has poor Wasm support

Resources

Comments