Building a Pure-Go Serverless WASM Runtime with Wazero
"How to execute untrusted user code safely at the edge using WebAssembly sandboxes in Go with zero CGO dependencies."
WebAssembly (WASM) has emerged as a lightweight, secure alternative to containers for running untrusted user scripts at the edge. While the WASM serverless landscape is heavily dominated by Rust and Wasmtime, it is entirely possible to construct a highly performant serverless runtime in pure Go.
Here is how we built a dynamic edge router leveraging wazero—a 100% pure Go WebAssembly runtime.
The Memory Bridge Challenge
When running guest WebAssembly modules (e.g., compiled from Go, Rust, or C) inside host systems, we must pass complex payloads (like HTTP headers and bodies). Because WebAssembly sandboxes have isolated linear memory, the host and guest cannot share pointers directly.
We solve this using a custom Memory Bridge. The host allocates memory inside the WASM instance, writes the request payload, and passes the pointer to the guest handler.
Here is a simplified snippet of how the host invokes a guest WASM module:
package main
import (
"context"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)
func runWasmHandler(ctx context.Context, mod wazero.CompiledModule, payload []byte) ([]byte, error) {
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)
// Instantiate the module
instance, err := r.InstantiateModule(ctx, mod, wazero.NewModuleConfig())
if err != nil {
return nil, err
}
// Get functions exported by the guest
malloc := instance.ExportedFunction("malloc")
handler := instance.ExportedFunction("handle_request")
free := instance.ExportedFunction("free")
// 1. Allocate memory inside the guest sandbox
results, err := malloc.Call(ctx, uint64(len(payload)))
if err != nil {
return nil, err
}
guestPtr := uint32(results[0])
defer free.Call(ctx, uint64(guestPtr))
// 2. Write payload directly to WebAssembly linear memory
instance.Memory().Write(guestPtr, payload)
// 3. Execute request handler
res, err := handler.Call(ctx, uint64(guestPtr), uint64(len(payload)))
if err != nil {
return nil, err
}
// 4. Decode returning pointer and size
resPtr := uint32(res[0] >> 32)
resLen := uint32(res[0])
// Read response bytes
out, _ := instance.Memory().Read(resPtr, resLen)
return out, nil
}
Performance Performance Comparison
By using pure Go sandboxing via wazero, we achieve scale-to-zero microsecond initialization speeds compared to standard Docker containers.
| Metric | Docker Containers | Wazero Go Sandbox |
|---|---|---|
| Cold Start Time | ~500ms - 3s | ~7ms |
| Idle RAM Overhead | ~100MB per service | 0MB (Instantiated on-demand) |
| Sandbox Size | 100MB - 1GB | ~3MB (WASM Bytecode) |
Conclusion
Building serverless execution environments in Go is highly practical. Using WebAssembly sandboxes, you can run multi-tenant user scripts safely on Edge nodes, IoT gateways, or servers with absolute isolation, minimal RAM overhead, and near-zero latency.