Back to blogBuilding a Pure-Go Serverless WASM Runtime with Wazero
GoWebAssemblyWASMServerless

Building a Pure-Go Serverless WASM Runtime with Wazero

July 20, 20263 min read

"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.