WebAssembly Component Model 2026: Rust + Go with Wasi-Cloud
What Is the WebAssembly Component Model?
For most of its history, WebAssembly was a superb single-language sandbox: compile Rust (or C++, or Go) to a flat .wasm binary and run it. Cross-language composition was bolted on via fragile memory-sharing conventions that every team reinvented differently. The WebAssembly Component Model, finalized in late 2025 and now broadly supported across all major runtimes, changes that fundamentally.
A component is a self-describing Wasm module that exports and imports typed interfaces defined in WIT (WebAssembly Interface Types). Two components — one written in Rust, one in Go — can be linked together at runtime without sharing a memory space, without FFI hacks, and without recompiling either side. Wasi-Cloud 0.3, released in January 2026, layers standard cloud primitives (HTTP server/client, key-value store, pub/sub messaging, scheduled tasks) on top of the Component Model, making it viable for production microservice workloads.
This tutorial walks you through building a Rust component that implements a text-processing interface, then consuming it from a Go component acting as an HTTP handler — all composed and hosted by Wasmtime 28 under the Wasi-Cloud 0.3 host ABI.
Prerequisites
- Rust 1.86+ with target
wasm32-wasip2installed (rustup target add wasm32-wasip2) - TinyGo 0.38+ — standard
go builddoes not yet emit Component Model binaries; TinyGo does - cargo-component 0.18+:
cargo install cargo-component - wasm-tools 1.220+:
cargo install wasm-tools - wasmtime CLI 28+:
curl -L https://wasmtime.dev/install.sh | bash - wit-bindgen-go 0.4+:
go install github.com/bytecodealliance/wit-bindgen-go/cmd/wit-bindgen-go@latest - Basic familiarity with Rust traits and Go interfaces
Step 1 — Define a WIT Interface
WIT is the contract language. Both the Rust producer and Go consumer derive their generated glue code from the same .wit file, so there is a single source of truth for the API.
Create the project layout:
mkdir wasi-bridge && cd wasi-bridge
mkdir -p wit rust-processor go-handler composed
Create wit/world.wit:
package techbytes:text-bridge@0.1.0;
interface processor {
/// Normalize whitespace, trim, and return word count alongside cleaned text.
record process-result {
cleaned: string,
word-count: u32,
}
process-text: func(input: string) -> process-result;
}
world rust-processor {
export processor;
}
world go-handler {
import processor;
export wasi:http/incoming-handler@0.3.0;
}
The two world declarations describe exactly which side exports and which imports the processor interface. wasm-tools uses these declarations during composition to wire the import to the export automatically.
Step 2 — Build the Rust Component
Initialise a cargo-component crate pointing at the WIT we just wrote:
cd rust-processor
cargo component new --lib processor
# Edit Cargo.toml to reference our shared WIT
Update rust-processor/Cargo.toml — add the WIT dependency block:
[package.metadata.component]
package = "techbytes:text-bridge"
[package.metadata.component.target]
path = "../wit"
world = "rust-processor"
Now implement the exported interface in src/lib.rs:
cargocomponentbindings::generate!();
use bindings::exports::techbytes::textbridge::processor::{Guest, ProcessResult};
struct Component;
impl Guest for Component {
fn processtext(input: String) -> ProcessResult {
let cleaned: String = input
.splitwhitespace()
.collect::<Vec<&str>>()
.join(" ");
let wordcount = cleaned.splitwhitespace().count() as u32;
ProcessResult { cleaned, wordcount }
}
}
bindings::export!(Component withtypesin bindings);
Build the component:
cargo component build --release
# Output: target/wasm32-wasip2/release/processor.wasm
Verify it is a valid Component Model binary (not a plain core module):
wasm-tools validate --features component-model \
target/wasm32-wasip2/release/processor.wasm
# Expected: (no output = valid)
Step 3 — Build the Go Consumer
Generate Go bindings from the shared WIT file, then implement the HTTP handler that calls into the Rust component via the imported interface.
cd ../go-handler
mkdir -p internal/gen
# Generate Go bindings
wit-bindgen-go generate \
--world go-handler \
--out internal/gen \
../wit/world.wit
Create main.go:
package main
import (
"fmt"
"net/http"
processor "github.com/example/wasi-bridge/go-handler/internal/gen/techbytes/text-bridge/processor"
wasihttp "github.com/bytecodealliance/wasi-go/http"
)
func init() {
wasihttp.Handle(http.HandlerFunc(handleRequest))
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
body := r.FormValue("text")
if body == "" {
body = " Hello WebAssembly Component Model "
}
result := processor.ProcessText(body)
fmt.Fprintf(w, {"cleaned":%q,"word_count":%d},
result.Cleaned, result.WordCount)
}
func main() {}
Build with TinyGo targeting wasip2:
tinygo build -o ../composed/handler.wasm \
-target wasip2 \
-wit-package techbytes:text-bridge \
-wit-world go-handler \
.
Step 4 — Compose & Run with Wasi-Cloud
We now have two .wasm components. wasm-tools compose links the Go component's import processor to the Rust component's export processor, producing a single self-contained artifact with no dangling imports (other than the host WASI APIs).
cd composed
# Copy the Rust component
cp ../rust-processor/target/wasm32-wasip2/release/processor.wasm ./
# Compose: handler imports processor, processor is provided by processor.wasm
wasm-tools compose handler.wasm \
--def processor.wasm \
-o composed-service.wasm
wasm-tools validate --features component-model composed-service.wasm
Run the composed service with wasmtime serve (Wasi-Cloud 0.3 HTTP host):
wasmtime serve --addr 127.0.0.1:8080 composed-service.wasm
In another terminal, send a request:
curl -s -X POST http://127.0.0.1:8080 \
-d 'text=The Component Model is production ready'
Key Insight: Zero Shared Memory Across Language Boundaries
When the Go handler calls processor.ProcessText(), the Component Model runtime serializes the string argument using the Canonical ABI — a deterministic encoding defined in the Component Model spec. The Rust component operates on its own linear memory; it never has read/write access to the Go component's heap. This isolation is architectural, not advisory, making cross-language composition safe by construction rather than by convention.
Verification & Expected Output
A successful POST should return:
{"cleaned":"The Component Model is production ready","word_count":6}
Confirm the binary size is reasonable (composed components are typically 300–900 KB before wasm-opt):
ls -lh composed-service.wasm
# Expected: something like 412K composed-service.wasm
# Optional: run wasm-opt for a ~30% size reduction
wasm-opt -O2 composed-service.wasm -o composed-service-opt.wasm
Inspect exported interfaces to confirm the composition is clean:
wasm-tools component wit composed-service.wasm
# Should show only wasi:http/incoming-handler as the top-level export
# The processor interface should NOT appear (it is internal after composition)
If you use our Code Formatter tool, you can paste the generated WIT output for quick syntax highlighting and sharing with your team.
Troubleshooting Top 3
1. type mismatch during wasm-tools compose
Symptom: error: import/export type mismatch for interface techbytes:text-bridge/processor
Cause: The Rust and Go components were built against different revisions of world.wit, causing the Canonical ABI encodings to diverge.
Fix: Ensure both crates reference the exact same wit/ directory. Commit wit/world.wit to a shared repo root and use relative paths in both Cargo.toml and the wit-bindgen-go invocation. Never copy-paste WIT files independently.
2. TinyGo build fails with undefined: processor.ProcessText
Symptom: TinyGo cannot resolve the generated binding symbol at link time.
Cause: wit-bindgen-go generates package paths that embed the WIT package name verbatim. If your Go module path does not match what wit-bindgen-go expects, the import path in main.go will be wrong.
Fix: Run wit-bindgen-go generate --dry-run ../wit/world.wit 2>&1 | grep package to see the exact package path emitted, then align your import statement to match.
3. wasmtime serve returns HTTP 500 with no log output
Symptom: Every request returns a 500 with a silent wasmtime process.
Cause: The Go handler panics at startup because wasihttp.Handle must be called inside an init() function, not main(). If it is called in main(), the WASI HTTP host dispatches the request before the handler is registered.
Fix: Move wasihttp.Handle(...) into an init() function (as shown in Step 3 above). Re-enable verbose logging with WASMTIMELOG=wasmtimewasi=debug wasmtime serve ... to surface similar startup errors in the future.
What's Next
You now have a working polyglot Wasm composition running under Wasi-Cloud 0.3. Here are the natural next steps:
- Add Wasi-Cloud KV storage — import
wasi:keyvalue/store@0.3.0in your Go world definition and bind to a provider like Fermyon Spin or wasmCloud for stateful handlers without a database sidecar. - Add a Python component —
componentize-py 0.14(Bytecode Alliance, 2026) can emit compliant components. The samewasm-tools composeworkflow applies, proving the Component Model is truly language-agnostic. - Deploy to a managed Wasi-Cloud host — Fastly Compute, Fermyon Cloud, and Cosmonic all support Component Model deployments as of Q1 2026. The composed binary you built in Step 4 deploys unmodified.
- Explore related architectures — Our deep-dives on building browser games with WASM + WebGPU in Rust and edge AI inference with LLMs on Wasm show what is possible when you extend this foundation.
Get Engineering Deep-Dives in Your Inbox
Weekly breakdowns of architecture, security, and developer tooling — no fluff.
Related Deep-Dives
Build Browser Games with WASM + WebGPU in Rust (2026)
A practical guide to combining WebAssembly and WebGPU with Rust for high-performance browser game development.
AI EngineeringEdge AI Inference 2026: LLMs on Wasm, Mobile, and IoT
How the WebAssembly runtime ecosystem is enabling LLM inference at the edge without cloud round-trips.
Cloud InfrastructureAWS Lambda Native Wasm GA: Technical Guide
Deep-dive into AWS Lambda's generally available WebAssembly execution environment and what it means for serverless architecture.