A pod running an ordinary container spends anywhere from hundreds of milliseconds to several seconds on startup — image pull, process boot, application runtime warm-up. For edge fan-out, short HTTP handlers, and load spikes that overhead adds up on every HPA scale-out event. SpinKube takes a different approach: a WebAssembly module gets scheduled by the same kubectl apply as a regular pod, but starts as a WASM instance — no extra layer, no new API, no different deploy path. Here’s how it works at the containerd level, and why it’s not “containers, but faster” — it’s a tool for a specific workload profile.
Table of contents
Open Table of contents
Why this matters in 2026
Server-side WebAssembly spent several years as a niche topic — first proofs of concept, then niche edge platforms with their own closed runtime. That changed on two fronts at once. Fermyon Spin — a framework and CLI for building WASM microservices — now handles roughly 75M RPS at the edge after Akamai’s acquisition of Fermyon, which is not a toy benchmark, it’s production load (source). At the same time, SpinKube landed in the CNCF Sandbox, and WASM modules are now scheduled directly through a containerd shim — built into the same path Kubernetes already uses to schedule regular containers, not bolted on through a sidecar or an external controller.
That shifts the question from “is server-side WASM worth trying at all” to “what share of my services already fit this profile today.” The answer isn’t “all of them,” and the rest of this post is about drawing that line honestly.
Architecture: one kubelet, two execution paths
The core idea behind SpinKube is that WASM doesn’t replace the node’s container runtime — it sits alongside it at the containerd shim level. containerd already has the right abstraction for this: the shim v2 API, which decides what process actually executes a workload. The default path is containerd-shim-runc-v2, which runs a Linux process in a namespace from an OCI image. containerd-shim-spin (built on the runwasi library) is a second, parallel path: it takes a .wasm component and spins up a Wasmtime instance for it instead of an OS process.
The choice of path isn’t made by a separate scheduler or an annotation — it’s the standard Kubernetes runtimeClassName field in the pod spec. A RuntimeClass with handler: spin tells kubelet to use the Spin shim instead of runc for that pod. Everything else in Kubernetes — Service, HPA, NetworkPolicy, readiness probes — keeps working unchanged, because to controllers it’s still an ordinary Pod with an ordinary Running status.
The SpinKube operator and the SpinApp CRD
Provisioning nodes by hand and tracking low-level shim details isn’t practical — that’s what the SpinKube operator is for: it handles node provisioning and turns a declarative Spin application description into ordinary Kubernetes objects.
Installation is three Helm releases: kwasm-operator installs the shim binary on nodes (enabled per-node via the kwasm.sh/node=true annotation, no node image rebuild required), cert-manager is the operator’s usual webhook dependency, and spin-operator itself:
helm install kwasm-operator kwasm-operator \
--repo https://kwasm.sh/kwasm-operator/ \
--namespace kwasm --create-namespace \
--set kwasmOperator.installerImage=ghcr.io/spinkube/containerd-shim-spin/node-installer:v0.21.0
kubectl annotate node --all kwasm.sh/node=true
helm install cert-manager cert-manager \
--repo https://charts.jetstack.io \
--namespace cert-manager --create-namespace \
--set crds.enabled=true
helm install spin-operator \
--namespace spin-operator --create-namespace --wait \
oci://ghcr.io/spinkube/charts/spin-operator
kubectl apply -f - <<'EOF'
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: wasmtime-spin-v2
handler: spin
EOF
From there, the app is built with the spin CLI, packaged as an OCI artifact, and described with the SpinApp CRD:
spin new -t http-rust hello-spin
cd hello-spin
spin build
spin registry push ttl.sh/hello-spin:1h
apiVersion: core.spinkube.dev/v1alpha1
kind: SpinApp
metadata:
name: hello-spin
spec:
image: "ttl.sh/hello-spin:1h"
executor: containerd-shim-spin
replicas: 3
resources:
limits:
cpu: "100m"
memory: "128Mi"
requests:
cpu: "50m"
memory: "32Mi"
kubectl apply -f spinapp.yaml — and the operator reconciles this into a Deployment with runtimeClassName: wasmtime-spin-v2 plus a Service in front of it. For the rest of the cluster — kubectl get pods, kubectl logs, HPA on CPU or custom metrics — there’s no difference from an ordinary container-backed service.
The honest limits of WASI 0.2
It’s worth resisting the marketing framing of “WASM is just faster containers.” The component model and WASI 0.2 (Preview 2) that Spin is built on are a sandbox with a deliberately narrow syscall surface, not a drop-in replacement for a Linux process:
- no full threading support — Spin’s model targets short-lived, largely single-threaded request handlers; WASI-threads proposals exist but aren’t a stable part of the ecosystem you can rely on in production;
- a trimmed-down network stack — outbound HTTP calls and Redis/Postgres connectors work through capabilities explicitly declared in the application manifest, arbitrary
socket()isn’t available; - not everything compiles to
wasm32-wasi— native C bindings, non-standard system libraries, heavy ML runtimes often simply don’t build for this target without a rewrite; - not for heavy stateful backends — long-lived connections, background workers with their own lifecycle, local disk as a source of truth — that’s not the profile Spin was designed for.
In other words: if a service is a wrapper around a blocking C driver or a long-lived stateful process, WASM on Kubernetes won’t help it and shouldn’t be treated as a replacement.
Where WASM actually wins
The workload profile where single-digit-millisecond cold starts and a lighter sandbox produce a measurable effect:
- edge functions — request handling close to the user, where instances scale up and down with traffic instead of running continuously;
- lightweight routing and API-gateway logic — request transformation, authorization, routing without heavy dependencies;
- bursty business logic — webhook handlers, short data transforms, where HPA needs to react in seconds, not tens of seconds spent waiting on an image pull;
- multi-tenant sandboxing — WASI isolation gives a narrower attack surface per process than an ordinary Linux namespace, at a comparable operational model.
There’s also a practical property worth calling out: WASM pods and ordinary container-backed Deployments coexist fine in the same cluster, even the same namespace. RuntimeClass is a pod-level field, not a cluster or namespace property, so migration isn’t all-or-nothing — you can move exactly the services that fit the profile above to Spin and leave the rest on runc.
What you need to compare startup and memory in practice
The simplest way to see the difference is to deploy a SpinApp next to an ordinary Deployment serving the same HTTP response, and run both through identical load.
kubectl scale deployment hello-container --replicas=0
kubectl scale spinapp hello-spin --replicas=0
kubectl scale deployment hello-container --replicas=1
kubectl get pod -l app=hello-container -o wide --watch
# ContainerCreating -> Running: usually hundreds of milliseconds
# to seconds, depending on whether the image is already cached on the node
kubectl scale spinapp hello-spin --replicas=1
kubectl get pod -l core.spinkube.dev/app-name=hello-spin -o wide --watch
# ContainerCreating -> Running: single-digit milliseconds after the first request
Load test — hey or k6 against both services with identical parameters:
hey -z 30s -c 50 http://hello-container.default.svc.cluster.local/
hey -z 30s -c 50 http://hello-spin.default.svc.cluster.local/
kubectl top pod -l app=hello-container
kubectl top pod -l core.spinkube.dev/app-name=hello-spin
Comparison table
| Dimension | Ordinary container (runc) | WASM app (SpinKube) |
|---|---|---|
| Cold start | hundreds of ms to seconds | single-digit ms |
| Idle memory footprint | tens to hundreds of MB per process | single-digit to tens of MB per instance |
| Threads / blocking I/O | full OS support | limited by WASI 0.2, no full threading |
| Network access | arbitrary socket() | capabilities declared in the app manifest |
| Compatibility with existing code | any Linux binary | only what builds for wasm32-wasi |
| Scheduling in Kubernetes | standard path | same kubectl apply, RuntimeClass |
| Coexistence in a cluster | — | yes, alongside regular pods in the same namespace |
| Best-fit profile | long-lived stateful services | short-lived stateless handlers |
How to verify it’s working
kubectl get spinapps
# NAME READY IMAGE
# hello-spin True ttl.sh/hello-spin:1h
kubectl get pods -l core.spinkube.dev/app-name=hello-spin
# NAME READY STATUS RESTARTS AGE
# hello-spin-6d8f9c7b6f-x2k9p 1/1 Running 0 4s
kubectl describe pod -l core.spinkube.dev/app-name=hello-spin | grep "Runtime Class"
# Runtime Class Name: wasmtime-spin-v2
curl -s http://hello-spin.default.svc.cluster.local/ -w '\n%{time_total}s\n'
If a pod stays in ContainerCreating for more than a couple of seconds, it almost always means the shim isn’t installed on the node (check the kwasm.sh/node annotation and the kwasm-operator logs) or the RuntimeClass wasn’t applied before the pod was created.
Bottom line
SpinKube doesn’t replace containers — it adds a second execution path next to the familiar runc one, reachable through the same runtimeClassName field, the same kubectl apply, the same HPA. The win is real and measurable for short-lived, stateless, I/O-light handlers: edge functions, routing, lightweight business logic — anywhere single-digit-millisecond cold starts and a smaller WASI sandbox attack surface outweigh Preview 2’s constraints. For heavy stateful backends, code with native dependencies, or services that need full threading, WASM isn’t an alternative yet — and shouldn’t be forced into being one. The right strategy for 2026 isn’t migrating a whole cluster — it’s picking the services that already fit this profile and running them alongside everything else with no extra component in the architecture.