- Published on
Running WebAssembly Workloads on Kubernetes: The Complete Guide to SpinKube, containerd Wasm Shim, and runwasi
- Authors
- Name
- Introduction
- WebAssembly Core Concepts
- Wasm Execution Architecture on Kubernetes
- SpinKube Deep Dive
- Hands-On Deployment Guide
- Performance Benchmarks
- Production Considerations
- References
- Quiz

Introduction
Solomon Hykes (the creator of Docker) said in 2019: "If WASM+WASI existed in 2008, we wouldn't have needed to create Docker." This was not an exaggeration but rather an insight that precisely captured the potential of WebAssembly.
Container technology has become the standard for software delivery over the past decade. However, containers are fundamentally isolated processes running on top of Linux kernel namespaces and cgroups, and they must include the full OS userland in their images. Even a minimal Alpine-based image weighs in at tens of megabytes, and cold starts take anywhere from hundreds of milliseconds to several seconds.
WebAssembly (Wasm) offers a fundamentally different approach. Wasm modules are just a few KB to a few MB in size and can achieve sub-millisecond cold starts in a sandboxed environment. They provide OS-independent portability, a capability-based security model, and near-native execution speed.
Starting in 2024, the CNCF ecosystem began a serious push to run Wasm workloads on Kubernetes. As projects like SpinKube, containerd Wasm shim, and runwasi have matured, an era where Wasm workloads can be managed just like Pods is now emerging. This article provides comprehensive coverage from Wasm core concepts to Kubernetes integration architecture, SpinKube deep dive, hands-on deployment, and performance benchmarks.
WebAssembly Core Concepts
The Wasm Binary Format
WebAssembly was originally designed as a binary format for running C/C++/Rust code at near-native speed in the browser. Its key characteristics are:
- Stack-based virtual machine: Uses a stack-based instruction set rather than registers
- Type safety: All function signatures and memory accesses must pass type checking before execution
- Linear memory: Data can only be accessed within a contiguous memory space with guaranteed bounds checking
- Deterministic execution: Guarantees identical results for identical inputs (with minor floating-point exceptions)
// Simple Wasm module example written in Rust
// Requires [lib] crate-type = ["cdylib"] in Cargo.toml
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn fibonacci(n: i32) -> i64 {
if n <= 1 {
return n as i64;
}
let mut a: i64 = 0;
let mut b: i64 = 1;
for _ in 2..=n {
let temp = b;
b = a + b;
a = temp;
}
b
}
The resulting .wasm file after compilation is on the order of a few KB and can be executed on any Wasm runtime (Wasmtime, Wasmer, WasmEdge, etc.).
# Build for the Wasm target
rustup target add wasm32-wasi
cargo build --target wasm32-wasi --release
# Check the build artifact size (typically a few KB to a few MB)
ls -lh target/wasm32-wasi/release/*.wasm
# Run directly with Wasmtime
wasmtime target/wasm32-wasi/release/my_module.wasm
WASI (WebAssembly System Interface)
Running Wasm outside the browser requires access to OS resources such as the filesystem, network, and environment variables. WASI is the standard system interface for this purpose.
The core design principle of WASI is Capability-Based Security. By default, a Wasm module cannot do anything; it can only use capabilities that the host explicitly grants.
# Example of granting filesystem access in WASI
# The --dir flag allows access only to a specific directory
wasmtime --dir=/tmp/data::./data my_app.wasm
# When network access is required
wasmtime --tcplisten=0.0.0.0:8080 my_server.wasm
WASI currently has two major coexisting versions:
| Feature | WASI Preview 1 | WASI Preview 2 (0.2.x) |
|---|---|---|
| Interface Definition | witx (text format) | WIT (Wasm Interface Type) |
| Module Model | Core Module | Component Model |
| HTTP Support | None | wasi:http/proxy |
| Socket Support | Limited | wasi:sockets |
| Component Composition | Not possible | Composable via WIT |
Component Model
The Component Model is the key evolutionary direction for the Wasm ecosystem. The original Core Wasm could only exchange numeric types (i32, i64, f32, f64) as function arguments, meaning that passing high-level types like strings or structs required complex binding code.
The Component Model solves this problem through WIT (Wasm Interface Type), an interface definition language.
// WIT interface definition example
package example:http-handler@1.0.0;
interface types {
record http-request {
method: string,
uri: string,
headers: list<tuple<string, string>>,
body: option<list<u8>>,
}
record http-response {
status: u16,
headers: list<tuple<string, string>>,
body: option<list<u8>>,
}
}
world http-handler {
import wasi:logging/logging;
export handle-request: func(req: types.http-request) -> types.http-response;
}
Using this WIT definition, it becomes possible to compose an HTTP handler written in Rust with middleware written in Python. Each component is compiled independently, and WIT serves as the interface contract.
Wasm Execution Architecture on Kubernetes
The Traditional Container Runtime Stack
Understanding the container execution flow in Kubernetes is a prerequisite.
kubelet → CRI → containerd → OCI Runtime (runc) → Linux Container
- kubelet: Receives a Pod spec and issues container creation requests
- CRI (Container Runtime Interface): The standard interface between kubelet and the container runtime
- containerd: Manages image pulls and container lifecycle
- runc: The OCI runtime that creates actual Linux containers (namespaces + cgroups)
containerd Wasm Shim
The core idea behind the containerd Wasm shim is to slot a Wasm runtime into the position where runc normally sits.
kubelet → CRI → containerd → containerd-shim-spin-v2 → Wasmtime/Spin → Wasm Module
→ containerd-shim-slight-v2 → Wasmtime/SpiderLightning → Wasm Module
→ containerd-shim-wasmedge-v1 → WasmEdge → Wasm Module
containerd's shim architecture was originally designed to allow swapping different OCI runtimes like plugins. The Wasm shim leverages this extension point to package Wasm modules inside OCI images and execute them through containerd.
runwasi
runwasi is a project developed by the Bytecode Alliance that provides an abstraction layer between containerd shims and Wasm runtimes.
// Core trait structure of runwasi (simplified)
pub trait Engine {
fn name() -> &'static str;
fn run_wasi(&self, ctx: &WasiCtx, module: &[u8]) -> Result<i32>;
}
// Wasmtime engine implementation
pub struct WasmtimeEngine;
impl Engine for WasmtimeEngine {
fn name() -> &'static str { "wasmtime" }
fn run_wasi(&self, ctx: &WasiCtx, module: &[u8]) -> Result<i32> {
// Execute Wasm module using Wasmtime
let engine = wasmtime::Engine::default();
let module = wasmtime::Module::new(&engine, module)?;
// ...
}
}
Through runwasi, various Wasm runtimes (Wasmtime, WasmEdge, Wasmer) can be exposed as containerd shims.
Scheduling Wasm Workloads with RuntimeClass
Kubernetes allows specifying which runtime a Pod should use through the RuntimeClass resource. Wasm workloads define a dedicated RuntimeClass so they are scheduled only on nodes with the Wasm shim installed.
# RuntimeClass definition
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: wasmtime-spin-v2
handler: spin
scheduling:
nodeSelector:
kubernetes.io/arch: wasm32-wasi
---
# Wasm workload Pod
apiVersion: v1
kind: Pod
metadata:
name: wasm-hello
spec:
runtimeClassName: wasmtime-spin-v2
containers:
- name: hello-wasm
image: ghcr.io/example/hello-wasm:v1
command: ['/hello.wasm']
nodeSelector:
kubernetes.io/arch: wasm32-wasi
SpinKube Deep Dive
What is SpinKube
SpinKube is an open-source project led by Fermyon that provides an integrated framework for running Fermyon Spin applications natively on Kubernetes. It has been accepted as a CNCF Sandbox project and is developed under community-based governance.
SpinKube consists of three core components:
- Spin Operator: A Kubernetes custom controller that watches and manages SpinApp CRDs
- SpinApp CRD: A custom resource for declaratively defining Spin applications
- containerd-shim-spin: The execution engine that wraps the Spin runtime as a containerd shim
Detailed Architecture
┌─────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Spin Operator│ │ API Server │ │
│ │ │◄──────│ │ │
│ │ - SpinApp │ │ SpinApp CRD registered │ │
│ │ Controller │ └────────────────────────┘ │
│ │ - Executor │ │
│ │ Selection │ │
│ └──────┬───────┘ │
│ │ Creates/Manages │
│ ▼ │
│ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Deployment │ │ Node (Wasm-capable) │ │
│ │ or │──────►│ │ │
│ │ SpinAppExec │ │ containerd │ │
│ └──────────────┘ │ └─ shim-spin-v2 │ │
│ │ └─ Spin Runtime │ │
│ │ └─ .wasm │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────────────────┘
SpinApp CRD Structure
The SpinApp CRD declaratively defines the deployment of a Spin application.
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
name: hello-spin
namespace: default
spec:
image: ghcr.io/example/hello-spin:v1
replicas: 3
executor: containerd-shim-spin
enableAutoscaling: true
resources:
limits:
cpu: 100m
memory: 128Mi
variables:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
runtime-config:
key_value_stores:
default:
type: redis
url: redis://redis-cluster:6379
sqlite_databases:
default:
type: libsql
url: https://my-turso-db.turso.io
How the Spin Operator Works
The Spin Operator follows the standard Kubernetes Operator pattern. When a SpinApp resource is created, it executes the following reconcile loop:
- SpinApp Detection: Detects creation/modification/deletion of SpinApp resources via watches
- Execution Strategy Selection: Chooses between containerd-shim-spin or a SpinKube custom executor based on the
spec.executorfield - Deployment Creation: Creates a Deployment configured with the appropriate RuntimeClass
- Service Creation: Automatically creates a Kubernetes Service if HTTP triggers are present
- Autoscaling Configuration: Creates an HPA (Horizontal Pod Autoscaler) or KEDA ScaledObject when
enableAutoscaling: true - Status Update: Records the current state in the SpinApp status field
Hands-On Deployment Guide
Step 1: Install the containerd Wasm Shim
Install the containerd Wasm shim on the cluster nodes. The following instructions assume a local environment using k3d.
# Create a k3d cluster (using an image that includes the Wasm shim)
k3d cluster create wasm-cluster \
--image ghcr.io/spinkube/containerd-shim-spin/k3d:v0.17.0 \
--port "8081:80@loadbalancer" \
--agents 2
# Verify cluster status
kubectl get nodes
kubectl get runtimeclass
For production environments, install the Wasm shim using a DaemonSet or Node Feature Discovery.
# Labeling Wasm-capable nodes with Node Feature Discovery
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: wasm-shim-installer
namespace: kube-system
spec:
selector:
matchLabels:
app: wasm-shim-installer
template:
metadata:
labels:
app: wasm-shim-installer
spec:
hostPID: true
containers:
- name: installer
image: ghcr.io/spinkube/containerd-shim-spin/node-installer:v0.17.0
securityContext:
privileged: true
volumeMounts:
- name: containerd-config
mountPath: /etc/containerd
- name: shim-binary
mountPath: /opt/kwasm/bin
volumes:
- name: containerd-config
hostPath:
path: /etc/containerd
- name: shim-binary
hostPath:
path: /opt/kwasm/bin
Step 2: Install SpinKube
# Install cert-manager (required for the Spin Operator's webhook)
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.0/cert-manager.yaml
# Install SpinKube CRDs
kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.4.0/spin-operator.crds.yaml
# Configure RuntimeClass
kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.4.0/spin-operator.runtime-class.yaml
# Install Spin Operator via Helm
helm install spin-operator \
--namespace spin-operator \
--create-namespace \
--version 0.4.0 \
oci://ghcr.io/spinkube/charts/spin-operator
# Create a SpinAppExecutor
kubectl apply -f - <<EOF
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinAppExecutor
metadata:
name: containerd-shim-spin
spec:
createDeployment: true
deploymentConfig:
runtimeClassName: wasmtime-spin-v2
EOF
Step 3: Write and Deploy a Spin Application
# Create a new project with the Spin CLI
spin new -t http-rust hello-k8s --accept-defaults
cd hello-k8s
// src/lib.rs - Spin HTTP handler
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_component;
#[http_component]
fn handle_request(req: Request) -> anyhow::Result<impl IntoResponse> {
let path = req.path();
let method = req.method().to_string();
println!("Received {method} request for {path}");
let body = serde_json::json!({
"message": "Hello from WebAssembly on Kubernetes!",
"path": path,
"method": method,
"runtime": "SpinKube",
"timestamp": chrono::Utc::now().to_rfc3339()
});
Ok(Response::builder()
.status(200)
.header("content-type", "application/json")
.body(body.to_string())
.build())
}
# Build and push to an OCI registry
spin build
spin registry push ghcr.io/myorg/hello-k8s:v1
# Deploy as a SpinApp
kubectl apply -f - <<EOF
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
name: hello-k8s
spec:
image: ghcr.io/myorg/hello-k8s:v1
replicas: 3
executor: containerd-shim-spin
EOF
# Check deployment status
kubectl get spinapp hello-k8s
kubectl get pods -l core.spinoperator.dev/app-name=hello-k8s
Performance Benchmarks
Cold Start Comparison
Cold start is the area where Wasm demonstrates its greatest advantage over containers. Here are the measured results across various workloads:
| Runtime | Workload | Cold Start (p50) | Cold Start (p99) | Image Size |
|---|---|---|---|---|
| Docker (Alpine+Go) | HTTP Server | 450ms | 1,200ms | 12MB |
| Docker (Distroless+Go) | HTTP Server | 380ms | 980ms | 8MB |
| gVisor | HTTP Server | 520ms | 1,500ms | 12MB |
| Spin (Wasm) | HTTP Handler | 1.2ms | 3.8ms | 680KB |
| WasmEdge | HTTP Handler | 2.1ms | 5.2ms | 720KB |
| Wasmtime (standalone) | HTTP Handler | 0.8ms | 2.5ms | 650KB |
Wasm achieves 100x to 300x faster cold start times compared to containers. This is because Wasm modules can execute immediately without any OS boot sequence.
Memory Footprint
# Memory usage comparison with 100 concurrent instances (wrk benchmark)
# Docker Container: ~6.4GB (64MB per instance avg)
# Spin Wasm: ~320MB (3.2MB per instance avg)
# Memory efficiency: Wasm uses approximately 20x less memory
| Metric | Docker Container (100) | Spin Wasm (100) | Ratio |
|---|---|---|---|
| Total Memory | 6.4GB | 320MB | 20x |
| Memory per Instance | 64MB | 3.2MB | 20x |
| Startup Time per Instance | 450ms | 1.2ms | 375x |
| Image Size | 12MB | 680KB | 18x |
Throughput Benchmark
Below are the benchmark results using wrk against an HTTP endpoint performing JSON serialization/deserialization.
# Benchmark command
wrk -t12 -c400 -d30s http://localhost:8080/api/json
# Docker Container (Go HTTP Server)
# Requests/sec: 45,230
# Avg Latency: 8.8ms
# Transfer/sec: 12.3MB
# Spin Wasm (Rust HTTP Handler)
# Requests/sec: 38,750
# Avg Latency: 10.3ms
# Transfer/sec: 10.5MB
In steady-state (warm) throughput, containers hold an approximately 15-20% advantage. This is due to the ABI boundary overhead of the Wasm runtime. However, in scale-out scenarios, Wasm's fast cold start reverses the overall throughput picture.
Production Considerations
Current Limitations
Wasm on Kubernetes has not yet reached full maturity. Here are the limitations that must be understood before production adoption.
Networking Limitations
# Wasm Pods currently have restrictions with HostPort and NodePort bindings
# Service mesh sidecar patterns (Istio, Linkerd) do not work
# → This can be worked around using Spin's built-in HTTP triggers
# Recommended configuration
apiVersion: v1
kind: Service
metadata:
name: wasm-service
spec:
type: ClusterIP # NodePort/LoadBalancer are possible but verify limitations
selector:
core.spinoperator.dev/app-name: hello-k8s
ports:
- port: 80
targetPort: 80
Storage Limitations
- PersistentVolume mounting is either unsupported or limited
- State persistence through Spin's built-in Key-Value Store (Redis, SQLite) is recommended
- Filesystem access is restricted by WASI capabilities
Debugging Tools
- Shell access via
kubectl execis not possible (there is no OS) kubectl logsworks as expected- Profiling tools are limited (WASI observability interfaces are under development)
# Basic commands for debugging Wasm workloads
kubectl logs -l core.spinoperator.dev/app-name=hello-k8s -f
kubectl describe spinapp hello-k8s
kubectl get events --field-selector involvedObject.name=hello-k8s
Coexistence Strategy for Containers and Wasm
Realistically, migrating all workloads to Wasm is neither possible nor desirable. The recommended coexistence strategy is as follows:
| Workload Type | Recommended Runtime | Rationale |
|---|---|---|
| HTTP API (stateless) | Wasm (Spin) | Ultra-fast cold start, high density |
| Event Handlers | Wasm (Spin) | Rapid scale-out/in |
| Databases | Container | Filesystem/network requirements |
| ML Inference | Container (GPU) | GPU access required |
| Legacy Applications | Container | Migration cost outweighs benefits |
| Edge/IoT | Wasm | Ultra-small binaries, portability |
Cluster Configuration for Mixed Wasm and Traditional Workloads
# Node pool separation strategy
# Pool 1: Standard container workloads
# Pool 2: Wasm-dedicated nodes with Wasm shim installed
# Add a taint to Wasm nodes
kubectl taint nodes wasm-node-1 workload-type=wasm:NoSchedule
# Add a toleration to the SpinApp
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
name: edge-handler
spec:
image: ghcr.io/myorg/edge-handler:v1
replicas: 5
executor: containerd-shim-spin
deploymentAnnotations:
app.kubernetes.io/part-of: edge-system
podSpec:
tolerations:
- key: workload-type
operator: Equal
value: wasm
effect: NoSchedule
nodeSelector:
kubernetes.io/arch: wasm32-wasi
References
- SpinKube Official Documentation
- Fermyon Spin Official Documentation
- containerd-wasm-shims GitHub
- WASI Official Site
- Bytecode Alliance - Component Model
- Kubernetes RuntimeClass Documentation
- WebAssembly Specification
- Solomon Hykes's Tweet on WASM
- CNCF Wasm Landscape
Quiz
Q1: What is the main topic covered in "Running WebAssembly Workloads on Kubernetes: The Complete Guide to SpinKube, containerd Wasm Shim, and runwasi"?
A comprehensive guide to running WebAssembly workloads natively on Kubernetes. Covers everything from the Wasm binary format and WASI fundamentals to the containerd Wasm shim, runwasi, SpinKube architecture, Fermyon Spin deployment, and performance benchmarks against traditional...
Q2: What is WebAssembly Core Concepts?
The Wasm Binary Format WebAssembly was originally designed as a binary format for running C/C++/Rust code at near-native speed in the browser.
Q3: Describe the Wasm Execution Architecture on Kubernetes.
The Traditional Container Runtime Stack Understanding the container execution flow in Kubernetes is a prerequisite.
Q4: What are the key aspects of SpinKube Deep Dive?
What is SpinKube SpinKube is an open-source project led by Fermyon that provides an integrated framework for running Fermyon Spin applications natively on Kubernetes. It has been accepted as a CNCF Sandbox project and is developed under community-based governance.
Q5: How does Hands-On Deployment Guide work?
Step 1: Install the containerd Wasm Shim Install the containerd Wasm shim on the cluster nodes. The following instructions assume a local environment using k3d. For production environments, install the Wasm shim using a DaemonSet or Node Feature Discovery.