| Standard | wazy | wazero |
|---|---|---|
| Core Wasm 1.0 / 2.0 | ✅ | ✅ |
WASI 0.1 (wasip1) |
✅ | ✅ |
WASI 0.2 · Component Model (wasip2) |
✅ | ❌ |
WASI 0.3 async ABI (wasip3) |
✅ | ❌ |
A fast WebAssembly runtime for Go: zero dependencies, no CGO, pure Go — with execution on par with wasmtime, plus the Component Model, WASI 0.2, and the WASI 0.3 async ABI running today.
wazy embeds WebAssembly in your Go application. Run code compiled from Rust, C, C++, TinyGo, Zig, and anything else that targets Wasm. No external toolchain. No cgo. Nothing to install at runtime. It is built for speed and developed aggressively — pure-Go convenience without giving up native-runtime performance, and it targets the modern Wasm platform: core modules, components, WASI 0.2, and the WASI 0.3 async ABI (below).
go get github.com/samyfodil/wazy@latestwazy is compliant with the WebAssembly Core Specification 1.0 and 2.0. It runs on any Go target, with an optimizing native compiler on amd64 and arm64, and a pure-Go interpreter everywhere else.
wazy is faster than wazero, the runtime it descends from, on the paths that set real throughput and latency — and it is on par with wasmtime on conformance, standards support, and speed, in pure Go with no CGO.
Measured on the same hardware:
| Path | wazy vs wazero | What & why |
|---|---|---|
| Host calls (Go ↔ Wasm) | up to ~15x | Typed host functions run at native-call speed with zero allocations — no reflect per call. The hot path for WASI and any host API you expose. |
memory.grow (in-capacity) |
~22x amd64 · 6.6x arm64 | Growth that fits an allocated reserve runs entirely in native code: no Go trampoline, no allocation, 0 allocs/op. wazero calls back into Go on every grow. Reserve with WithMemoryCapacityReservePages. |
Interruptible loops (WithCloseOnContextDone) |
~12–13x; +5% vs +75% | The per-loop safety check is amortized, not a Go round-trip per iteration. Overhead scales to the loop body; tune with WithInterruptCheckInterval. |
| Interpreter | ~30% | Per-call heap allocation eliminated — a benchmark that allocated 1.35M times now allocates twice. |
| Compiled execution | 4–18% (geomean ~6%) | Memory-heavy code leads: strings −18%, array reversal −14%, base64 −12%. Also allocates up to −17% less per module on real Rust/Zig/C output. |
| Memory per call | ~87% less | The common request-per-call pattern. |
| Cold start | substantial | Decode, validate, compile, instantiate — all faster, with far fewer allocations. |
Deadline/cancellation enforcement and the memory.grow reserve are opt-in per compile (WithCloseOnContextDone(true), WithMemoryCapacityReservePages); out-of-capacity and shared/imported memories keep the safe Go path.
Methodology and per-optimization numbers are in OPTIMIZATIONS.md. The head-to-head suite lives in benchmarks/vs-wazero — cd benchmarks/vs-wazero && go test -bench . runs the same workloads (compile, execution, host calls) on wazy and wazero side by side. The same harness carries a three-way comparison against wasmtime (BenchmarkExecute3, BenchmarkCompile3): run it yourself and see wazy's native execution hold its own against Cranelift on the compute kernels — no numbers to take on faith.
The host-call speedup comes from dropping reflection. Instead of the usual reflect-per-call path, which is ~14x slower, typed generic helpers derive the Wasm signature from Go's types at compile time:
wazy.HostFunc2(builder, func(ctx context.Context, mod api.Module, x, y uint32) uint32 {
return x + y
}).Export("add")HostFunc0–HostFunc8 and HostProc0–HostProc8 cover most functions. WithGoModuleFunction handles the rest. All zero-allocation.
wazy runs WebAssembly Component Model components and WASI 0.2 — genuine wasm32-wasip2 binaries (rustc, wasm-tools), not just core modules and not hand-written .wat. wazero targets neither. This works today, exercised by real rustc components and the official component-model test suites:
- The Canonical ABI — lift and lower for every value type (primitives,
string,list,record,variant,enum,flags,option,result,tuple) andown/borrowresource handles (drop/rep, cross-instance borrows), verified byte-for-byte against thewasm-toolsvalue suites. - WASI 0.2 host interfaces —
wasi:cli(run, stdio, environment, exit),wasi:clocks,wasi:filesystem,wasi:io(streams, poll, error),wasi:random,wasi:sockets(TCP/UDP and DNS), andwasi:http(both the incoming-handler server and the outgoing-handler client). - Multi-module component graphs — nested instances, canonical lowering of host imports, resource lifetimes, and the wasip2 adapter wiring, so a real rustc
wasi:cli/commandruns end to end and printshello world.
Embed and run a component through the component package:
r := wazy.NewRuntime(ctx)
defer r.Close(ctx)
inst, err := component.Instantiate(ctx, r, componentWasm,
component.WithWASI(component.WASIConfig{Stdout: os.Stdout})...)
if err != nil {
return err
}
defer inst.Close(ctx)
// A wasi:cli/command component: run its entry point.
_, err = inst.Call(ctx, "wasi:cli/run@0.2.3#run")Call an interface export directly with inst.CallExport("component:adder/calc", "add", uint32(2), uint32(3)), or serve a wasi:http/incoming-handler component straight to net/http — *component.Instance satisfies http.Handler. The API is young and, like the rest of wazy, makes no stability promise. See examples/component for a runnable interface-export call, a WASI 0.2 command, and an async export end to end.
wazy runs the Component Model's async ABI: components that suspend, await, and resume — the model WASI 0.3 is built on. wazero has none of it, and no other pure-Go runtime does either.
- Callback and stackful lift — both async lift shapes. A guest task that returns WAIT/YIELD is driven by a deterministic per-composition scheduler; a stackful task suspends on a goroutine with an unbuffered-channel baton, so exactly one runs at a time — race-free by construction, verified under
-race. - Streams and futures —
stream<T>/future<T>with rendezvous copy and per-elementown<R>resource transfer, synchronous and asynchronous read/write, and cancellation. - Task lifecycle — subtasks, cancellation, backpressure, context-local storage, and borrow scopes that hold across async calls.
thread.*— a cooperative fiber runtime (thread.new-indirect,yield,suspend,yield-then-resume) built on the same goroutine-plus-baton primitive.
It passes all 31 official Component Model async .wast conformance suites (one carries a fixture fix filed upstream as component-model#679), cross-checked by a differential trace-oracle that byte-compares wazy against the spec reference (definitions.py). Goroutines and channels back futures, streams, and threads naturally — the one place Go's substrate is an asset over the hand-written event loops other runtimes need.
An async export is called exactly like a synchronous one — the scheduler runs underneath and Call returns when the task completes. examples/component runs an async export and a thread-spawning component (thread.new-indirect + thread.yield-then-resume) end to end.
wazy targets the modern Wasm platform (Component Model, WASI 0.2, the async ABI today, above; the full WASI 0.3 host-interface surface next) and lands performance work continuously rather than waiting on a release cadence. wazero, the runtime it descends from, is a mature, well-run project that prioritizes API stability and a scope centered on core modules and WASI 0.1 — the right call for its large user base. wazy makes different trade-offs.
Two things make that pace possible:
- We ship fast. wazy makes no API-stability promise. It has already broken compatibility with wazero, including host-function registration, and will do so again whenever that makes the runtime faster or moves it toward the Component Model. Correctness is guarded by the conformance and fuzzing suites, not by freezing the API.
- We accept AI contributions. Machine-generated changes are welcome on equal footing with human ones — the bar is the same for both: they pass the full spec-conformance, differential, and fuzzing suites, and they make the runtime measurably better.
wazy.NewRuntime(ctx) picks the optimizing compiler when the platform supports it, amd64 or arm64, and falls back to the interpreter otherwise. You can force either:
r := wazy.NewRuntimeWithConfig(ctx, wazy.NewRuntimeConfigInterpreter())- Compiler translates each module to machine code during
CompileModule, so your functions run natively, typically an order of magnitude faster than interpretation, with no host-specific dependencies. - Interpreter is pure Go with no architecture-specific code, so it runs anywhere Go runs, down to targets like
riscv64.
The fastest way in is an example. The basic one extends a Go program with an addition function written in WebAssembly.
- taubyte/tau — the open-source platform behind Taubyte clouds runs WebAssembly workloads on wazy.
Using wazy in production? Open a PR to add yourself.
wazy started from wazero's code (Copyright 2020-2023 wazero authors) and still draws on its WebAssembly semantics, WASI implementation, and compliance and fuzzing test suites. We do not intend to keep wazero's API compatibility or its architecture. The goals are pure Go, performance, and conformance to the standard. See RATIONALE.md for wazero's original design rationale and LICENSE for the Apache 2.0 license.
Apache 2.0. See LICENSE.