Skip to content
Stribog

Runtime

All writing

WebAssembly on Kubernetes: SpinKube, runwasi and the Portable Runtime

Running WebAssembly on Kubernetes: the runwasi shim, SpinKube and wasmCloud, the honest limits, and why Wasm is the exit ramp from proprietary serverless.

Stribog14 min read

Containers already solved portability, and they solved it well enough that nobody argues about it. An OCI image built in CI runs on a laptop, a bare-metal cluster, and three clouds. That is settled infrastructure, and WebAssembly does not improve on it in any way that would justify a migration.

The unsolved layer sits directly above. The moment a team adopts a function platform — Lambda, Cloud Functions, Workers, a managed edge runtime — the artifact stops being portable. The handler signature, the event envelope, the bindings to queues and object storage, the cold-start behaviour the code was tuned around: all of it belongs to one vendor. There is no docker run equivalent. Repatriating a fleet of functions is not a redeployment, it is a rewrite, which is exactly why so many organisations that have otherwise completed a cloud repatriation still have a function estate stranded behind them.

That is the gap WebAssembly closes. The WebAssembly System Interface and the Component Model define what a compiled unit may import from its host — HTTP, sockets, clocks, key-value stores, a filesystem view — as a typed contract rather than a vendor SDK. A component that imports wasi:http/incoming-handler runs on any host that exports it. This article is about what that costs and what it actually buys once the artifact has to live on a Kubernetes cluster you operate.

What changes when you swap the shim

The integration point is smaller than most people expect, and that is the whole reason this is tractable. Kubernetes never executes anything itself; the kubelet issues CRI calls to containerd, and containerd delegates to a shim binary chosen by the runtime handle on the Pod. Swapping the executor means adding a second shim to the node and a RuntimeClass that points at it. Nothing about the scheduler, the API server, the CNI, or your admission policy changes.

The seam is containerd's shim table. Everything above it — scheduler, API server, CNI, admission control — is unchanged, which is why this can be introduced on a node pool rather than a cluster.

runwasi is the library that makes this possible: a containerd shim framework, maintained under the containerd project itself, that adapts a Wasm engine to the shim protocol containerd already speaks. It is not an end-user tool. What you deploy is a shim built on it — most commonly containerd-shim-spin-v2, which runs Spin applications and is one of the three components of SpinKube alongside the Spin Operator and the Runtime Class Manager. Other shims built on the same library run plain WASI command modules or WasmEdge.

Node-level installation deserves care, because it mutates the container runtime on a live node. The Runtime Class Manager makes it declarative — installing and upgrading shim binaries, patching the containerd configuration, labelling provisioned nodes, and removing them cleanly. On an immutable node OS the mechanics differ: with Talos Linux there is no package manager and no writable /usr to drop a binary into, so the shim ships as a system extension in the boot assets and the containerd patch is a machine-config fragment. More work up front, considerably less drift after — the trade Talos makes everywhere else too.

yaml
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: wasmtime-spin-v2
handler: spin
scheduling:
  nodeSelector:
    spin.kwasm.io/wasm-runtime: "true"
  tolerations:
    - key: "runtime"
      operator: "Equal"
      value: "wasm"
      effect: "NoSchedule"
---
apiVersion: core.spinkube.dev/v1alpha1
kind: SpinApp
metadata:
  name: quote-pricing
spec:
  image: "registry.internal.example.com/pricing/quote:1.4.0"
  executor: containerd-shim-spin
  replicas: 3
  runtimeConfig:
    loadFromSecret: quote-pricing-runtime-config
  resources:
    limits:
      memory: "64Mi"
      cpu: "100m"
The runtime handle must match the shim binary name containerd expects — containerd-shim-spin-v2 on the node resolves to the handle spin.

Two details in that manifest matter more than they look. The nodeSelector and taint keep Wasm workloads on a node pool that has the shim, so a scheduling accident cannot land a .wasm artifact on a node that will fail to start it with an unhelpful error. And the memory limit is not a typo — 64 MiB is generous for a component whose entire artifact is often under 5 MB, which changes bin-packing arithmetic in a way that is easy to under-appreciate until you look at node counts.

Enforce the pairing rather than trusting it. A Kyverno rule that requires a matching nodeSelector whenever runtimeClassName is set, and rejects Pods that request a runtime class the cluster has not provisioned, turns a class of confusing scheduling failures into a clear admission denial — the same pattern covered in our guide to policy as code with Kyverno.

Three credible hosts, one artifact

The temptation is to treat SpinKube as the answer and stop. It is the most direct answer for a Kubernetes estate, but understanding the alternatives is what makes the choice reversible — and reversibility is the entire argument for being here.

SpinKube — Wasm as an ordinary Kubernetes workload

SpinKube is the least disruptive option because it makes Wasm look like everything else. A SpinApp custom resource is reconciled into a Deployment and a Service; HPA, network policy, service mesh, Prometheus scraping, and your existing GitOps pipeline all apply unchanged. The operator handles the runtime configuration secret — the mapping from a component's declared imports (a key-value store, a SQL database, an outbound HTTP allow-list) to the concrete backends it is permitted to reach. Spin and SpinKube were contributed to the CNCF by Fermyon, which turns out to have mattered rather a lot; more on that below.

wasmCloud — a lattice that is not Kubernetes-shaped

wasmCloud, a CNCF incubating project since November 2024, takes a different position: components are scheduled onto a lattice of hosts connected over NATS, with declarative Open Application Model manifests reconciled by wadm. Hosts can be inside Kubernetes, on an edge box, or on a developer's machine, and the lattice spans them. That is a genuinely different distribution model, and it is the right one when the topology is not cluster-shaped — a fleet of retail sites, a telecoms edge, industrial gateways. It also means a second control plane to operate, on top of a NATS backbone. If you already run NATS or a self-managed event backbone, that cost is mostly paid; if not, it is real.

wasmtime serve — the option that keeps everyone honest

The third host is not a platform at all. wasmtime serve app.wasm runs a wasi:http component behind a listener, from a single static binary, under systemd, on a machine with no orchestrator. It is not how you run a fleet. It is how you prove the artifact is not captive — and it is the fallback that makes a bad platform decision recoverable in an afternoon rather than a quarter. Verify it in CI on every build; a portability claim that is never exercised decays silently.

bash
# Build once — the component, not a platform-specific bundle.
cargo component build --release
wasm-tools validate target/wasm32-wasip2/release/quote.wasm

# Inspect the contract: what the component imports is what a host must provide.
wasm-tools component wit target/wasm32-wasip2/release/quote.wasm | grep -E 'import|export'
#   import wasi:http/[email protected]
#   import wasi:keyvalue/[email protected]
#   export wasi:http/[email protected]

# Host 1 — no orchestrator, no platform, single binary.
wasmtime serve -Scommon target/wasm32-wasip2/release/quote.wasm

# Host 2 — push as an OCI artifact, run under the shim on Kubernetes.
wkg oci push registry.internal.example.com/pricing/quote:1.4.0 \
  target/wasm32-wasip2/release/quote.wasm
kubectl apply -f spinapp.yaml

# Host 3 — the same bytes on a lattice host, scheduled by wadm.
wash app deploy ./wadm.yaml
A portability check worth running in CI: the same artifact, three hosts, no rebuild.

The wit output is the substance of the sovereignty claim. The component declares that it needs an outgoing HTTP handler and a key-value store. It names no vendor, region, SDK version, or endpoint. Which store satisfies that import is a deployment-time decision made by the operator, not a compile-time dependency welded into the code — and that is the thing no function platform has ever offered.

The numbers, and what they are not

The published figures are real and consistently reported: community and vendor benchmarks put Wasm instantiation in the low single-digit milliseconds against roughly 50–500 ms for a container start, and artifact sizes under 10 MB against container images that routinely run to hundreds. Fermyon's managed platform advertises sub-millisecond cold starts. These are not our measurements and you should not plan against them without running your own, but the order of magnitude is not in dispute.

What matters is which of those numbers changes a decision. Artifact size mostly does not — registries are cheap and image pulls are cached. Cold start does, but only for workloads where it was already a design constraint. If a service holds 200 warm replicas because scaling from zero costs half a second of user-visible latency, a millisecond instantiation removes the reason those replicas exist. Combined with event-driven autoscaling and scale-to-zero, that is the difference between a node pool sized for peak and one sized for the current minute.

The second honest caveat is throughput. Per-request execution inside Wasmtime is generally slower than native code — the gap has narrowed considerably with ahead-of-time compilation, but it has not closed. For a CPU-bound service running flat out, Wasm is a tax. For a bursty, I/O-bound, request-response service that spends most of its life idle, the density gain dwarfs the per-request cost. Knowing which of those describes your workload is the entire analysis.

Security: a real sandbox, not a free pass

The security model is the strongest technical argument here and the one most often overstated. A Wasm component has no ambient authority. It cannot open a file, resolve a name, or make an outbound connection unless the host explicitly grants that capability. There is no /proc to read, no syscall surface to probe, no shell to drop into, no package manager whose CVE backlog you inherit. Compare that to a container, where the isolation is a negotiated subset of a shared kernel and the default posture is closer to permit-then-restrict.

For multi-tenant execution of code you did not write — customer plugins, user-supplied transforms, partner extensions — this is a categorically better starting position than a container, and it is the use case where Wasm is least arguable.

Now the qualifications. The shim itself runs as a privileged process on the node, so a Wasmtime escape is a node compromise; Wasmtime has a documented CVE history and a security process, and you must patch it like any other runtime. Capability grants are only as tight as the runtime configuration you write, and a permissive outbound HTTP allow-list rebuilds the ambient authority you just removed. Runtime detection tooling is weaker here: eBPF-based sensors such as Falco and Tetragon observe syscalls, and a component executing entirely inside a host process makes very few of them — the sandbox that hides an attacker from the kernel also hides them from your detection layer, and the compensating control is host-level instrumentation plus capability audit rather than syscall telemetry.

Supply chain is the third gap. Wasm artifacts push to an OCI registry and sign with cosign like any image, and that part works today. Dependency provenance is less mature: SBOM tooling for composed components is still catching up with the Component Model, so a composed artifact can be harder to attest than a container built from a well-understood base image. Fold it into the same SBOM, Sigstore and SLSA pipeline you already run, and be explicit in your audit evidence about where component-level provenance is currently thinner than image-level provenance. Auditors respond well to a documented gap and badly to a discovered one.

Where it breaks

Every honest evaluation of this technology is a list of things it cannot do yet. Ours:

  • Threads remain unresolved. Shared-memory threading in WASI is still not settled. Anything whose performance depends on a thread pool is not a candidate.
  • Language support is uneven. Rust and TinyGo are first-class. JavaScript and Python work through componentisation but drag an interpreter into the artifact, erasing much of the size advantage. Languages with heavy managed runtimes remain awkward.
  • Long-lived and stateful workloads are the wrong shape. Databases, brokers, anything holding a large working set or a persistent connection pool belongs in a container. This is a request-response and event-handler runtime.
  • Observability is thinner. Distributed tracing through components works but the instrumentation ecosystem is younger; expect to do more wiring by hand than your OpenTelemetry and Prometheus stack requires for ordinary services.
  • Debugging is harder. There is no kubectl exec into a Wasm sandbox, because there is no shell. Logs, traces and local reproduction are the whole toolkit.
  • Networking is constrained. wasi:sockets is narrower than a Linux socket API. Pods still get a normal cluster IP and normal Cilium network policy applies at the Pod boundary, but exotic protocol work is out of scope.
  • The talent pool is small. Fewer engineers have debugged a Wasm host in anger than have debugged a container. That is a real staffing risk on a system you intend to run for a decade.

None of these are fatal for the workloads Wasm suits. All of them are fatal if you adopt it as a general container replacement, which is the failure mode we would expect to see most often over the next two years.

The exit ramp, in both directions

No runtime choice earns a place in a serious architecture without a documented way out. This one has an unusually cheap one, and it is worth stating precisely because it is asymmetric.

Leaving Wasm is a repackaging exercise: the same bytes plus a runtime binary in a scratch image, back on runc. Entering it is not — that direction needs a rebuild from source.

Leaving is cheap. A .wasm component plus a Wasmtime binary in a scratch image is an ordinary OCI container that runs on runc on any cluster. You lose the density and the cold-start profile and you keep the code, the interfaces, and the deployment topology. That fallback can be built pre-emptively in CI and left dormant, which is the form an exit ramp should take: tested, not theoretical.

Entering is not cheap, and the asymmetry is the thing to plan around. There is no meaningful container-to-Wasm conversion — you cannot wrap an existing image and get a component. Migration means recompiling from source against WASI interfaces, replacing anything that assumed a filesystem, a thread pool, or an arbitrary syscall. Budget it as a port, scope it to services that are already stateless and small, and never let a migration plan assume a mechanical translation exists.

Governance, and the long game

The most instructive event in this ecosystem was not technical. Akamai announced its acquisition of Fermyon — the company behind Spin and the principal author of SpinKube — on 1 December 2025. Roughly eight months earlier, Fermyon had contributed Spin and SpinKube to the CNCF.

The projects had already left the building before the building changed owners. That sequencing is the entire difference between a dependency and a liability.

Had those projects still been single-vendor repositories under a company acquired by a CDN with its own edge-compute strategy, every organisation running them would now be re-evaluating a load-bearing dependency on someone else's roadmap. Because governance had already moved to a foundation with open contribution and public technical steering, the acquisition changed the sponsor and not the project. This is open source as method rather than as a licence file: the point is not that a fork is permitted, but that the governance makes one unnecessary.

The same lens applies to the standards. WASI 0.3.0 was released on 11 June 2026, adding native async to the Component Model — async func, stream<T>, future<T> — and refactoring the 0.2 interfaces around those primitives, with support in Wasmtime 43 and later. It removes the most common reason teams bounced off WASI 0.2: async work previously meant hand-rolled state machines or polling loops. The Bytecode Alliance has named WASI 1.0, with long-term support commitments, as the next milestone, targeting late 2026 or early 2027.

Read that timeline honestly. A standard reaching 1.0 next year is not a settled standard today. If your planning horizon is a decade, the correct posture is a deliberate first workload now and a broader commitment after 1.0 ships — early enough that the interfaces you build against are the ones being stabilised, late enough that you are not carrying the cost of every pre-1.0 breaking change across a fleet.

How to start without betting anything

The right first move is small, real, and reversible. Pick one stateless, bursty, low-consequence service — a webhook receiver, an image-metadata extractor, an internal API shim. Not the most important one, and not a toy.

  1. Provision a separate node pool with the shim installed via the Runtime Class Manager, tainted so nothing lands there by accident. Never introduce a new runtime to a general-purpose pool.
  2. Port one service and keep the container version deployable behind a feature flag or a weighted route for the whole evaluation window.
  3. Measure on your own hardware: p50 and p99 latency, cold-start distribution, memory per replica, and node-level density against the container baseline.
  4. Build the scratch-image fallback in the same pipeline from day one and run its smoke tests on every build, so the exit ramp is proven rather than assumed.
  5. Write the capability grants down as reviewed configuration — outbound allow-lists, key-value bindings, filesystem preopens — and treat that file as a security artifact, because it is.
  6. Publish the runtime class as a documented golden path only once a second team has shipped on it without your help.

If, after a quarter, the density and cold-start numbers do not justify a second runtime in your estate, delete the node pool and keep the container. That outcome is a successful evaluation, not a failed one — and the fact that it costs a node pool rather than a rewrite is precisely the property that made the experiment worth running.

For edge estates the calculation shifts. A 1–2 MB runtime footprint on constrained hardware changes what is deployable at all, and the portability argument compounds with the disconnected-operation patterns in our guide to K3s and Fleet at the edge. That is where we would expect the first genuinely load-bearing WebAssembly deployments in most estates — not in the datacentre, where containers are already good enough.

§FAQ/Common questions

Frequently asked

Is WebAssembly on Kubernetes production-ready in 2026?

For the right workload shape, yes. The containerd shim path via runwasi is stable, SpinKube and wasmCloud are CNCF projects with real deployments, and WASI 0.3.0 shipped native async in June 2026. It is production-ready for stateless, bursty, request-response and event-handler services. It is not ready as a general container replacement: threading is unresolved, languages with heavy managed runtimes are awkward, stateful workloads are the wrong shape, and the standard itself does not reach 1.0 until late 2026 or early 2027.

Do I need a separate cluster to run Wasm workloads?

No. The shim is a node-level component, so a labelled and tainted node pool inside your existing cluster is enough. Wasm Pods get a normal cluster IP, network policy, service discovery, and Prometheus scraping. The dedicated pool is a scheduling-safety measure, not an isolation requirement — a Pod requesting a runtime class its node cannot serve fails confusingly, and a taint prevents that.

How do I migrate an existing container workload to WebAssembly?

You recompile from source; there is no conversion path from an OCI image to a component. Target wasm32-wasip2 with a Component Model toolchain, replace anything assuming a thread pool, an arbitrary syscall, or an unrestricted filesystem, and declare the host capabilities you need in WIT. Scope migrations to services that are already stateless and small. The reverse direction is much cheaper: a component plus a Wasmtime binary in a scratch image runs on runc anywhere.

What did the Akamai acquisition of Fermyon mean for SpinKube?

Structurally, very little — which was the point. Fermyon contributed Spin and SpinKube to the CNCF roughly eight months before Akamai announced the acquisition on 1 December 2025, so governance had already moved to a foundation with open contribution before the corporate event. Akamai stated it would continue supporting the open-source projects and maintain Bytecode Alliance membership. The lesson generalises: check where a project's governance sits before it becomes load-bearing, not after.

Is a Wasm sandbox more secure than a container?

It has a better default posture. A component has no ambient authority — no filesystem, network, or clock access unless the host grants it — and no shell, package manager, or syscall surface to attack, which makes it a strong fit for executing untrusted third-party code. But the shim runs privileged on the node, Wasmtime has its own CVE stream to patch, permissive capability grants rebuild the authority you removed, and eBPF-based detection sees far fewer syscalls from a component than from a container.

WebAssembly Kubernetes production 2026SpinKube Kubernetes wasm workloadsrunwasi containerd wasm shimwasmCloud WASI component modelWASI 0.3 async Kubernetes workloadscontainer to wasm migration portability

Executive Briefing

Thirty minutes to clarify your infrastructure risk

Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.