WASI Host Runtimes: wasmtime, wazero & Spin
A wasip1/wasm binary is only half the story.
Search across all documentation pages
A wasip1/wasm binary is only half the story.
Something must instantiate the module, satisfy WASI imports, and optionally map HTTP triggers or filesystem capabilities.
wasmtime, wazero, and Spin are the three hosts Go teams reach for first - CLI tooling, in-process embedding, and edge HTTP respectively.
Compile Go with GOOS=wasip1 GOARCH=wasm, then run the artifact through a host that speaks WASI Preview 1.
wasmtime excels at local dev and scripting.
wazero fits Go microservices that load plugins in-process.
Spin packages opinionated HTTP handlers for Fermyon-style edge deploys.
Each host differs in how it grants filesystem, environment, and network capabilities.
Quick-reference recipe card - copy-paste ready.
# Dev loop
GOOS=wasip1 GOARCH=wasm go build -o app.wasm .
wasmtime run --env APP_ENV=dev --dir .::/work app.wasm// Embed with wazero (Go host)
wasi_snapshot_preview1.MustInstantiate(ctx, runtime)
mod, _ := runtime.Instantiate(ctx, wasmBytes)
_, _ = mod.ExportedFunction("_start").Call(ctx)When to reach for this:
Guest (guest/main.go):
package main
import (
"fmt"
"os"
)
func main() {
name, _ := os.Hostname()
fmt.Println("hostname:", name)
fmt.Println("env:", os.Getenv("GREETING"))
}Build:
GOOS=wasip1 GOARCH=wasm go build -o guest.wasm ./guestwasmtime:
wasmtime run --env GREETING=hello --dir .::/ guest.wasmwazero host (host/main.go):
package main
import (
"context"
"os"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)
func main() {
ctx := context.Background()
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)
wasi_snapshot_preview1.MustInstantiate(ctx, r)
cfg := wazero.NewModuleConfig().
WithEnv("GREETING", "hello from wazero").
WithFSConfig(wazero.NewFSConfig().WithDirMount(".", "/"))
wasm, _ := os.ReadFile("guest.wasm")
mod, _ := r.InstantiateWithConfig(ctx, wasm, cfg)
defer mod.Close(ctx)
_, _ = mod.ExportedFunction("_start").Call(ctx)
}Spin (spin.toml excerpt + build):
spin_manifest_version = 2
[application]
name = "go-guest"
version = "0.1.0"
[[trigger.http]]
route = "/..."
component = "guest"
[component.guest]
source = "guest.wasm"
[component.guest.trigger]
route = "/"spin build
spin upWhat this demonstrates:
_start (or exported main wrappers) runs after instantiation.| Runtime | Language | cgo | Typical use | HTTP story |
|---|---|---|---|---|
| wasmtime | Rust core, multi-language bindings | Optional in bindings | CLI, polyglot platforms | Via WASI HTTP proposals / embed custom |
| wazero | Pure Go | No | Go services embedding plugins | You mount net/http around guest calls |
| Spin | Rust runtime + Go/other guest langs | No for Go guests | Edge microservices | First-class HTTP triggers |
Call bound CPU time; combine with fuel metering APIs when available.// Narrow plugin ABI: guest reads stdin JSON, writes stdout JSON.
// Host wraps Call with buffers via ModuleConfig stdin/stdout overrides.Missing WASI instantiate in wazero - Forgetting wasi_snapshot_preview1.MustInstantiate yields obscure import errors. Fix: always register WASI before guest modules.
Assuming working directory - Guests see only pre-opened paths, not the host CWD. Fix: mount . explicitly to / or /work.
Spin networking expectations - Not every Go net/http server compiles to a Spin component without adaptation. Fix: follow Spin Go SDK patterns for HTTP handlers.
wasmtime version skew - Older hosts lack newer WASI snapshots. Fix: pin wasmtime in CI to match production.
Re-instantiating per request without caching - Compilation dominates cold latency. Fix: cache compiled modules; reuse wazero.CompiledModule.
Passing sensitive host paths - Over-broad --dir /::/ breaks sandboxing. Fix: mount least-privilege directories per guest.
| Alternative | Use When | Don't Use When |
|---|---|---|
| Wasmer | You already standardize on it | Team wants pure-Go embedder (wazero) |
| Node + experimental WASI | JS shop, tiny scripts | Production Go embedding |
Native plugins (.so) | Maximum performance, trusted code | Untrusted tenant code |
| Firecracker microVMs | Strong isolation beyond WASM | Sub-millisecond cold starts required |
wazero is pure Go - simpler static binaries, no cgo, natural context propagation.
wasmtime remains excellent for CLI/dev and polyglot hosts.
Yes. Instantiate multiple modules on one runtime, or isolate runtimes per tenant for memory separation.
No. Spin runs gc-compiled wasip1 modules, but size limits may push you toward TinyGo or -ldflags=-s -w.
fmt.Println to stdout/stderr maps to host logs.
Capture in wazero via ModuleConfig stdout writers.
Yes via custom imports in wazero (DefineModuleBuilder) or wasmtime linker APIs.
Keep the ABI narrow and versioned.
Check each runtime's WASI sockets support.
Spin exposes HTTP at the trigger layer instead of raw sockets for many apps.
Treat .wasm as immutable artifacts versioned in object storage.
Swap module bytes and re-instantiate; Spin handles via spin deploy.
wazero supports execution timeouts via context; advanced metering evolves per release - verify docs for your pinned version.
Yes if WASI imports match.
Test both in CI when you embed in Go but develop with wasmtime CLI.
Use WASI-mapped paths with read-only mounts for inputs and writable mounts only where persistence is required.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026