{"schemaVersion":1,"id":"https://lucioduran.com/blog/rust-wasm-component-model-production","url":"https://lucioduran.com/blog/rust-wasm-component-model-production","title":"Rust + WASM Component Model in Production: Polyglot Microservices That Actually Work","description":"Building production polyglot microservices with Rust, the WASM Component Model, and WIT interfaces. Cross-language composition, wasm-tools workflows, and hard lessons from running this at scale.","language":"en","datePublished":"2025-10-08","dateModified":null,"contentFormat":"text/markdown","content":"WebAssembly\n\n# Rust + WASM Component Model in Production: Polyglot Microservices That Actually Work\n\n2025-10-088 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/rust-wasm-component-model-production>)\n\n## Component Model Definition\n\nA [WASM Component](<https://component-model.bytecodealliance.org/>) is:\n\n1. A ` .wasm ` binary that follows the Component Model spec (not a \"core\" wasm module)\n2. It declares typed **imports** (what it needs) and **exports** (what it provides)\n3. These types are defined in **WIT** (WebAssembly Interface Type) files\n4. Components can be **composed**, wired together at their import/export boundaries\n5. Each component has its own linear memory (sandboxed by default)\n\nHere's a minimal WIT definition for an authentication component:\n\n```wit\n// auth.wit\npackage myorg:auth@1.0.0;\n\ninterface authenticate {\n record auth-request {\n token: string,\n required-scopes: list<string>,\n }\n\n record auth-result {\n authenticated: bool,\n user-id: option<string>,\n granted-scopes: list<string>,\n error: option<string>,\n }\n\n verify: func(req: auth-request) -> auth-result;\n}\n\nworld auth-component {\n export authenticate;\n}\n```\n\nAnd here's the Rust implementation:\n\n```rust\n// src/lib.rs\nwit_bindgen::generate!({\n world: \"auth-component\",\n path: \"../wit/auth.wit\",\n});\n\nstruct AuthComponent;\n\nimpl Guest for AuthComponent {\n fn verify(req: AuthRequest) -> AuthResult {\n let claims = match decode_jwt(&req.token) {\n Ok(c) => c,\n Err(e) => return AuthResult {\n authenticated: false,\n user_id: None,\n granted_scopes: vec![],\n error: Some(format!(\"JWT decode failed: {e}\")),\n },\n };\n\n let granted: Vec<String> = req.required_scopes\n .iter()\n .filter(|s| claims.scopes.contains(s))\n .cloned()\n .collect();\n\n let all_granted = granted.len() == req.required_scopes.len();\n\n AuthResult {\n authenticated: all_granted,\n user_id: Some(claims.sub.clone()),\n granted_scopes: granted,\n error: if all_granted { None } else {\n Some(\"Insufficient scopes\".into())\n },\n }\n }\n}\n\nexport!(AuthComponent);\n```\n\nThe ` wit_bindgen::generate! ` macro reads the WIT file and generates all the type definitions and glue code. You implement the trait, export the struct, build it:\n\n```bash\ncargo component build --release\n```\n\nThat produces a ` .wasm ` component (not a core module) that any Component Model runtime can load and call.\n\n## Cross-Language Composition: The Real Power\n\nHere's where it gets interesting. Say your auth component is in Rust, but your API gateway is in Go, and your business logic is in Python (using componentize-py). Each is compiled to a separate WASM component.\n\nFirst, the Go gateway component's WIT:\n\n```wit\n// gateway.wit\npackage myorg:gateway@1.0.0;\n\ninterface handle-request {\n record http-request {\n method: string,\n path: string,\n headers: list<tuple<string, string>>,\n body: option<list<u8>>,\n }\n\n record http-response {\n status: u16,\n headers: list<tuple<string, string>>,\n body: list<u8>,\n }\n\n handle: func(req: http-request) -> http-response;\n}\n\nworld gateway-component {\n // Import auth from the Rust component\n import myorg:auth/authenticate@1.0.0;\n // Import business logic from the Python component\n import myorg:bizlogic/process@1.0.0;\n\n export handle-request;\n}\n```\n\nThe Go implementation imports the auth interface and calls it like a local function, no HTTP, no serialization:\n\n```go\npackage main\n\nimport (\n \"myorg/gateway/gen\"\n auth \"myorg/auth/gen\"\n bizlogic \"myorg/bizlogic/gen\"\n)\n\ntype Gateway struct{}\n\nfunc (g *Gateway) Handle(req gen.HttpRequest) gen.HttpResponse {\n // Extract bearer token\n token := extractBearerToken(req.Headers)\n\n // Call the Rust auth component, same process, no network\n authResult := auth.Verify(auth.AuthRequest{\n Token: token,\n RequiredScopes: []string{\"read:users\"},\n })\n\n if !authResult.Authenticated {\n return gen.HttpResponse{\n Status: 401,\n Body: []byte(`{\"error\": \"unauthorized\"}`),\n }\n }\n\n // Call the Python business logic component\n result := bizlogic.Process(bizlogic.ProcessRequest{\n UserId: *authResult.UserId,\n Action: \"list_users\",\n Payload: req.Body,\n })\n\n return gen.HttpResponse{\n Status: 200,\n Headers: []gen.Header{{\"content-type\", \"application/json\"}},\n Body: result.Data,\n }\n}\n```\n\nNow compose them:\n\n```bash\n# Build each component\ncargo component build --release -p auth-component\ntinygo build -target=wasip2 -o gateway.wasm ./gateway\ncomponentize-py -d ./wit -w bizlogic-component ./bizlogic.py -o bizlogic.wasm\n\n# Compose into a single linked component\nwasm-tools compose \\\n --definitions auth-component.wasm \\\n --definitions bizlogic.wasm \\\n -o composed-service.wasm \\\n gateway.wasm\n```\n\nThe ` composed-service.wasm ` is a single binary containing all three components, with their imports/exports wired together. You can run it on wasmtime, [Cloudflare](<https://cloudflare.com/>) Workers, Fermyon Spin, or any Component Model runtime.\n\n## The Build Pipeline in Detail\n\nGetting this working in CI takes more effort than expected. Here is a Makefile that actually works:\n\n```makefile\nWIT_DIR := ./wit\nBUILD_DIR := ./build\nWASI_ADAPTER := ./adapters/wasi_snapshot_preview1.reactor.wasm\n\n.PHONY: all clean validate compose\n\nall: validate compose\n\n# Validate all WIT files first\nvalidate:\n\twasm-tools component wit $(WIT_DIR) --check\n\n# Build Rust auth component\n$(BUILD_DIR)/auth.wasm: auth/src/lib.rs $(WIT_DIR)/auth.wit\n\tcd auth && cargo component build --release\n\tcp auth/target/wasm32-wasip2/release/auth_component.wasm $@\n\twasm-opt -Oz $@ -o $@\n\n# Build Go gateway component\n$(BUILD_DIR)/gateway.wasm: gateway/main.go $(WIT_DIR)/gateway.wit\n\tcd gateway && tinygo build -target=wasip2 -o ../$@\n\twasm-tools component new $@ --adapt $(WASI_ADAPTER) -o $@\n\n# Build Python bizlogic component\n$(BUILD_DIR)/bizlogic.wasm: bizlogic/app.py $(WIT_DIR)/bizlogic.wit\n\tcomponentize-py -d $(WIT_DIR) -w bizlogic-component bizlogic/app.py -o $@\n\n# Compose everything\ncompose: $(BUILD_DIR)/auth.wasm $(BUILD_DIR)/gateway.wasm $(BUILD_DIR)/bizlogic.wasm\n\twasm-tools compose \\\n\t\t-d $(BUILD_DIR)/auth.wasm \\\n\t\t-d $(BUILD_DIR)/bizlogic.wasm \\\n\t\t-o $(BUILD_DIR)/composed-service.wasm \\\n\t\t$(BUILD_DIR)/gateway.wasm\n\t@echo \"Composed binary: $$(du -h $(BUILD_DIR)/composed-service.wasm | cut -f1)\"\n\nclean:\n\trm -rf $(BUILD_DIR)/*\n```\n\nTwo important lessons:\n\n1. **WIT validation must come first.** A typo in a WIT file produces inscrutable errors downstream. ` wasm-tools component wit --check ` catches them early.\n2. **The WASI adapter is version-sensitive.** TinyGo, Rust's ` wasm32-wasip2 ` target, and componentize-py each expect a specific WASI preview version. The adapter binary should be pinned in the repo and referenced explicitly.\n\n## Performance: Tradeoff Analysis\n\nHere are real numbers from a production deployment. The setup has 12 WASM components (mix of Rust, Go, and Python) composed into 3 services, running on a custom wasmtime 18 orchestrator on bare metal.\n\n**Component call overhead (inter-component function call):**\n\n| Payload size | Latency | Notes |\n| --- | --- | --- |\n| Empty (no args) | 0.8μs | Baseline dispatch cost |\n| 1KB struct | 2.1μs | Includes canonical ABI serialization |\n| 64KB blob | 18μs | Dominated by memcpy between linear memories |\n| 1MB blob | 156μs | Linear with size, as expected |\n\nFor context, a gRPC call over loopback is ~50μs for an empty message, and ~200μs for 1KB. So component calls are ~25x faster than gRPC for small payloads. The gap narrows with large payloads because both are bottlenecked on data copying.\n\n**CPU overhead vs native (compute-bound benchmark, no I/O):**\n\n| Component language | vs native | Notes |\n| --- | --- | --- |\n| Rust → WASM | 1.15x | Minimal overhead, bounds checks |\n| Go → WASM (TinyGo) | 1.45x | TinyGo's GC is simpler, some opts missing |\n| Python → WASM | 3.2x | componentize-py includes CPython interpreter |\n\nThe Python number looks bad, but remember: this Python component runs in the same process, with no container overhead, no cold start, and full sandboxing. If the alternative is a Python container with 800ms cold start, 3.2x CPU overhead on a 5ms computation is a fantastic trade.\n\n**Memory footprint:**\n\n| Deployment | Memory per instance |\n| --- | --- |\n| 12 Docker containers (1 per component) | ~3.2GB total |\n| 3 composed WASM services (4 components each) | ~420MB total |\n| Single composed WASM (all 12 components) | ~180MB |\n\nThe memory savings are dramatic because WASM components share the runtime (wasmtime) and only allocate their own linear memory. A Rust component's linear memory starts at 64KB and grows on demand.\n\n## The Sandboxing Story\n\nThis is the part that sells security teams. Each WASM component:\n\n- Cannot access the filesystem unless explicitly given a ` wasi:filesystem ` capability\n- Cannot make network calls unless given ` wasi:sockets `\n- Cannot read another component's memory (separate linear memories)\n- Cannot execute arbitrary syscalls (WASI capability model)\n\nConsider a third-party vendor providing a \"data enrichment\" component. In the container world, this would need to run in a separate network namespace, configure AppArmor/SELinux policies, set up egress filtering, etc. With WASM, the component can be composed into the service with zero capabilities, it can compute on the data passed to it and return results. That's it. If the code tries to phone home, the call fails at the WASI boundary, not at a network firewall.\n\n```bash\n# Compose vendor component with NO WASI capabilities\nwasm-tools compose \\\n --definitions vendor-enrichment.wasm \\\n --definitions our-service.wasm \\\n -o final.wasm \\\n orchestrator.wasm\n\n# The vendor component can only:\n# - Receive data through its WIT-defined imports\n# - Return data through its WIT-defined exports\n# - Use CPU and its own linear memory\n# It CANNOT: open files, make network calls, read env vars, spawn processes\n```\n\n## Common Mistakes to Avoid\n\n**Mistake 1: Putting JSON parsing at the component boundary.** A common first design has components exchange JSON strings. This meant every cross-component call paid for serialize + deserialize + two string allocations. Switching to WIT ` record ` types with the canonical ABI eliminated this entirely.\n\n**Mistake 2: One giant component instead of composition.** A common initial approach compiles everything into a single WASM module (not component). This worked but lost all the sandboxing benefits and made updates impossible without redeploying everything. The Component Model's composition is the whole point, it lets you update one component without touching others.\n\n**Mistake 3: Ignoring the adapter version.** Three days can be lost debugging a segfault that turned out to be a mismatch between the WASI preview 1 adapter and a component built for preview 2. Pin your adapter versions. Seriously.\n\n## When to Use This (and When Not To)\n\nUse the WASM Component Model when:\n\n- You need to compose code from multiple languages in a single process\n- Security isolation between components is important\n- You want high deployment density (many services, limited memory)\n- Your services are compute-heavy with small data exchanges\n\nDon't use it when:\n\n- All your code is one language (just use that language's native tooling)\n- You need to exchange large datasets between components (the copy overhead adds up)\n- Your team isn't comfortable with the build toolchain complexity\n- You need mature debugging/profiling tools (they're getting better, but not there yet)\n\nThe Component Model isn't replacing Kubernetes or Docker. It's a new tool for a specific class of problems, and for those problems, it's remarkably effective.\n\n## Frequently asked questions\n\n### Is the WASM Component Model ready for production use?\n\nFor specific use cases, yes. We've been running Rust WASM components on Cloudflare Workers and in a custom wasmtime-based orchestrator since Q3 2025. The Component Model spec reached 1.0 in mid-2025, and tooling like wasm-tools 1.2+ and cargo-component 0.16+ is stable. The gap is in ecosystem maturity, most libraries don't expose WIT interfaces yet.\n\n### How does cross-language composition actually work?\n\nEach component is a self-contained .wasm binary with typed imports and exports defined in WIT. A Go component can import a function exported by a Rust component, and the runtime (wasmtime, wamr, etc.) handles the ABI translation. The key constraint is that components don't share memory, all data crossing boundaries is copied through the canonical ABI.\n\n### What's the performance overhead compared to native microservices?\n\nFor CPU-bound logic, benchmarks show 1.2-1.8x overhead vs native Rust, mostly from bounds checking and indirect calls. For I/O-bound services (which is most real-world code), the overhead is negligible because you're waiting on network/disk anyway. The win is in deployment density, 40 WASM components can run in the memory footprint of 8 native containers.\n\n## Tips\n\n1. **Pin your WIT versions in a git submodule:** WIT interfaces are your API contracts. Put them in a dedicated repo and reference as a submodule. When you run ` wasm-tools component new `, pass ` --adapt wasi_snapshot_preview1=adapter.wasm ` to pin the WASI adapter version too. Version drift between WIT and adapter causes silent ABI mismatches.\n2. **Use ` cargo component build --release ` with LTO:** The default debug build produces .wasm files 5-10x larger. Adding ` lto = true ` and ` opt-level = 's' ` in your Cargo.toml release profile, combined with ` wasm-opt -Oz `, typically gets a component from 2MB to 180KB.\n3. **Avoid ` String ` in WIT interface boundaries:** Every string crossing the component boundary requires an allocation + copy because components have separate linear memories. Use ` list<u8> ` with a known encoding, or better, define ` record ` types with fixed-size fields. Production data shows a 40% latency reduction on a JSON-heavy interface by switching to structured records.\n4. **Test component linking locally with ` wasmtime compose `:** Before deploying, run ` wasm-tools compose -d deps/ -o linked.wasm your-component.wasm ` to catch interface mismatches at build time. The error messages from runtime linking are cryptic; the compose tool gives you clear diagnostics.\n\n- rust\n- webassembly\n- component-model\n- wit\n- wasm-tools\n- microservices\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"Rust + WASM Component Model in Production: Polyglot Microservices That Actually Work","url":"https://lucioduran.com/blog/rust-wasm-component-model-production#title"},{"level":2,"text":"Component Model Definition","url":"https://lucioduran.com/blog/rust-wasm-component-model-production#component-model-definition"},{"level":2,"text":"Cross-Language Composition: The Real Power","url":"https://lucioduran.com/blog/rust-wasm-component-model-production#cross-language-composition-the-real-power"},{"level":2,"text":"The Build Pipeline in Detail","url":"https://lucioduran.com/blog/rust-wasm-component-model-production#the-build-pipeline-in-detail"},{"level":2,"text":"Performance: Tradeoff Analysis","url":"https://lucioduran.com/blog/rust-wasm-component-model-production#performance-tradeoff-analysis"},{"level":2,"text":"The Sandboxing Story","url":"https://lucioduran.com/blog/rust-wasm-component-model-production#the-sandboxing-story"},{"level":2,"text":"Common Mistakes to Avoid","url":"https://lucioduran.com/blog/rust-wasm-component-model-production#common-mistakes-to-avoid"},{"level":2,"text":"When to Use This (and When Not To)","url":"https://lucioduran.com/blog/rust-wasm-component-model-production#when-to-use-this-and-when-not-to"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/rust-wasm-component-model-production#questions"},{"level":3,"text":"Is the WASM Component Model ready for production use?","url":"https://lucioduran.com/blog/rust-wasm-component-model-production#question-1"},{"level":3,"text":"How does cross-language composition actually work?","url":"https://lucioduran.com/blog/rust-wasm-component-model-production#question-2"},{"level":3,"text":"What's the performance overhead compared to native microservices?","url":"https://lucioduran.com/blog/rust-wasm-component-model-production#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/rust-wasm-component-model-production#article-tips"}],"sections":[{"heading":"Rust + WASM Component Model in Production: Polyglot Microservices That Actually Work","level":1,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production","content":"WebAssembly"},{"heading":"Rust + WASM Component Model in Production: Polyglot Microservices That Actually Work","level":1,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production#title","content":"2025-10-088 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/rust-wasm-component-model-production>)"},{"heading":"Component Model Definition","level":2,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production#component-model-definition","content":"A [WASM Component](<https://component-model.bytecodealliance.org/>) is:\n\n1. A ` .wasm ` binary that follows the Component Model spec (not a \"core\" wasm module)\n2. It declares typed **imports** (what it needs) and **exports** (what it provides)\n3. These types are defined in **WIT** (WebAssembly Interface Type) files\n4. Components can be **composed**, wired together at their import/export boundaries\n5. Each component has its own linear memory (sandboxed by default)\n\nHere's a minimal WIT definition for an authentication component:\n\n```wit\n// auth.wit\npackage myorg:auth@1.0.0;\n\ninterface authenticate {\n record auth-request {\n token: string,\n required-scopes: list<string>,\n }\n\n record auth-result {\n authenticated: bool,\n user-id: option<string>,\n granted-scopes: list<string>,\n error: option<string>,\n }\n\n verify: func(req: auth-request) -> auth-result;\n}\n\nworld auth-component {\n export authenticate;\n}\n```\n\nAnd here's the Rust implementation:\n\n```rust\n// src/lib.rs\nwit_bindgen::generate!({\n world: \"auth-component\",\n path: \"../wit/auth.wit\",\n});\n\nstruct AuthComponent;\n\nimpl Guest for AuthComponent {\n fn verify(req: AuthRequest) -> AuthResult {\n let claims = match decode_jwt(&req.token) {\n Ok(c) => c,\n Err(e) => return AuthResult {\n authenticated: false,\n user_id: None,\n granted_scopes: vec![],\n error: Some(format!(\"JWT decode failed: {e}\")),\n },\n };\n\n let granted: Vec<String> = req.required_scopes\n .iter()\n .filter(|s| claims.scopes.contains(s))\n .cloned()\n .collect();\n\n let all_granted = granted.len() == req.required_scopes.len();\n\n AuthResult {\n authenticated: all_granted,\n user_id: Some(claims.sub.clone()),\n granted_scopes: granted,\n error: if all_granted { None } else {\n Some(\"Insufficient scopes\".into())\n },\n }\n }\n}\n\nexport!(AuthComponent);\n```\n\nThe ` wit_bindgen::generate! ` macro reads the WIT file and generates all the type definitions and glue code. You implement the trait, export the struct, build it:\n\n```bash\ncargo component build --release\n```\n\nThat produces a ` .wasm ` component (not a core module) that any Component Model runtime can load and call."},{"heading":"Cross-Language Composition: The Real Power","level":2,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production#cross-language-composition-the-real-power","content":"Here's where it gets interesting. Say your auth component is in Rust, but your API gateway is in Go, and your business logic is in Python (using componentize-py). Each is compiled to a separate WASM component.\n\nFirst, the Go gateway component's WIT:\n\n```wit\n// gateway.wit\npackage myorg:gateway@1.0.0;\n\ninterface handle-request {\n record http-request {\n method: string,\n path: string,\n headers: list<tuple<string, string>>,\n body: option<list<u8>>,\n }\n\n record http-response {\n status: u16,\n headers: list<tuple<string, string>>,\n body: list<u8>,\n }\n\n handle: func(req: http-request) -> http-response;\n}\n\nworld gateway-component {\n // Import auth from the Rust component\n import myorg:auth/authenticate@1.0.0;\n // Import business logic from the Python component\n import myorg:bizlogic/process@1.0.0;\n\n export handle-request;\n}\n```\n\nThe Go implementation imports the auth interface and calls it like a local function, no HTTP, no serialization:\n\n```go\npackage main\n\nimport (\n \"myorg/gateway/gen\"\n auth \"myorg/auth/gen\"\n bizlogic \"myorg/bizlogic/gen\"\n)\n\ntype Gateway struct{}\n\nfunc (g *Gateway) Handle(req gen.HttpRequest) gen.HttpResponse {\n // Extract bearer token\n token := extractBearerToken(req.Headers)\n\n // Call the Rust auth component, same process, no network\n authResult := auth.Verify(auth.AuthRequest{\n Token: token,\n RequiredScopes: []string{\"read:users\"},\n })\n\n if !authResult.Authenticated {\n return gen.HttpResponse{\n Status: 401,\n Body: []byte(`{\"error\": \"unauthorized\"}`),\n }\n }\n\n // Call the Python business logic component\n result := bizlogic.Process(bizlogic.ProcessRequest{\n UserId: *authResult.UserId,\n Action: \"list_users\",\n Payload: req.Body,\n })\n\n return gen.HttpResponse{\n Status: 200,\n Headers: []gen.Header{{\"content-type\", \"application/json\"}},\n Body: result.Data,\n }\n}\n```\n\nNow compose them:\n\n```bash\n# Build each component\ncargo component build --release -p auth-component\ntinygo build -target=wasip2 -o gateway.wasm ./gateway\ncomponentize-py -d ./wit -w bizlogic-component ./bizlogic.py -o bizlogic.wasm\n\n# Compose into a single linked component\nwasm-tools compose \\\n --definitions auth-component.wasm \\\n --definitions bizlogic.wasm \\\n -o composed-service.wasm \\\n gateway.wasm\n```\n\nThe ` composed-service.wasm ` is a single binary containing all three components, with their imports/exports wired together. You can run it on wasmtime, [Cloudflare](<https://cloudflare.com/>) Workers, Fermyon Spin, or any Component Model runtime."},{"heading":"The Build Pipeline in Detail","level":2,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production#the-build-pipeline-in-detail","content":"Getting this working in CI takes more effort than expected. Here is a Makefile that actually works:\n\n```makefile\nWIT_DIR := ./wit\nBUILD_DIR := ./build\nWASI_ADAPTER := ./adapters/wasi_snapshot_preview1.reactor.wasm\n\n.PHONY: all clean validate compose\n\nall: validate compose\n\n# Validate all WIT files first\nvalidate:\n\twasm-tools component wit $(WIT_DIR) --check\n\n# Build Rust auth component\n$(BUILD_DIR)/auth.wasm: auth/src/lib.rs $(WIT_DIR)/auth.wit\n\tcd auth && cargo component build --release\n\tcp auth/target/wasm32-wasip2/release/auth_component.wasm $@\n\twasm-opt -Oz $@ -o $@\n\n# Build Go gateway component\n$(BUILD_DIR)/gateway.wasm: gateway/main.go $(WIT_DIR)/gateway.wit\n\tcd gateway && tinygo build -target=wasip2 -o ../$@\n\twasm-tools component new $@ --adapt $(WASI_ADAPTER) -o $@\n\n# Build Python bizlogic component\n$(BUILD_DIR)/bizlogic.wasm: bizlogic/app.py $(WIT_DIR)/bizlogic.wit\n\tcomponentize-py -d $(WIT_DIR) -w bizlogic-component bizlogic/app.py -o $@\n\n# Compose everything\ncompose: $(BUILD_DIR)/auth.wasm $(BUILD_DIR)/gateway.wasm $(BUILD_DIR)/bizlogic.wasm\n\twasm-tools compose \\\n\t\t-d $(BUILD_DIR)/auth.wasm \\\n\t\t-d $(BUILD_DIR)/bizlogic.wasm \\\n\t\t-o $(BUILD_DIR)/composed-service.wasm \\\n\t\t$(BUILD_DIR)/gateway.wasm\n\t@echo \"Composed binary: $$(du -h $(BUILD_DIR)/composed-service.wasm | cut -f1)\"\n\nclean:\n\trm -rf $(BUILD_DIR)/*\n```\n\nTwo important lessons:\n\n1. **WIT validation must come first.** A typo in a WIT file produces inscrutable errors downstream. ` wasm-tools component wit --check ` catches them early.\n2. **The WASI adapter is version-sensitive.** TinyGo, Rust's ` wasm32-wasip2 ` target, and componentize-py each expect a specific WASI preview version. The adapter binary should be pinned in the repo and referenced explicitly."},{"heading":"Performance: Tradeoff Analysis","level":2,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production#performance-tradeoff-analysis","content":"Here are real numbers from a production deployment. The setup has 12 WASM components (mix of Rust, Go, and Python) composed into 3 services, running on a custom wasmtime 18 orchestrator on bare metal.\n\n**Component call overhead (inter-component function call):**\n\n| Payload size | Latency | Notes |\n| --- | --- | --- |\n| Empty (no args) | 0.8μs | Baseline dispatch cost |\n| 1KB struct | 2.1μs | Includes canonical ABI serialization |\n| 64KB blob | 18μs | Dominated by memcpy between linear memories |\n| 1MB blob | 156μs | Linear with size, as expected |\n\nFor context, a gRPC call over loopback is ~50μs for an empty message, and ~200μs for 1KB. So component calls are ~25x faster than gRPC for small payloads. The gap narrows with large payloads because both are bottlenecked on data copying.\n\n**CPU overhead vs native (compute-bound benchmark, no I/O):**\n\n| Component language | vs native | Notes |\n| --- | --- | --- |\n| Rust → WASM | 1.15x | Minimal overhead, bounds checks |\n| Go → WASM (TinyGo) | 1.45x | TinyGo's GC is simpler, some opts missing |\n| Python → WASM | 3.2x | componentize-py includes CPython interpreter |\n\nThe Python number looks bad, but remember: this Python component runs in the same process, with no container overhead, no cold start, and full sandboxing. If the alternative is a Python container with 800ms cold start, 3.2x CPU overhead on a 5ms computation is a fantastic trade.\n\n**Memory footprint:**\n\n| Deployment | Memory per instance |\n| --- | --- |\n| 12 Docker containers (1 per component) | ~3.2GB total |\n| 3 composed WASM services (4 components each) | ~420MB total |\n| Single composed WASM (all 12 components) | ~180MB |\n\nThe memory savings are dramatic because WASM components share the runtime (wasmtime) and only allocate their own linear memory. A Rust component's linear memory starts at 64KB and grows on demand."},{"heading":"The Sandboxing Story","level":2,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production#the-sandboxing-story","content":"This is the part that sells security teams. Each WASM component:\n\n- Cannot access the filesystem unless explicitly given a ` wasi:filesystem ` capability\n- Cannot make network calls unless given ` wasi:sockets `\n- Cannot read another component's memory (separate linear memories)\n- Cannot execute arbitrary syscalls (WASI capability model)\n\nConsider a third-party vendor providing a \"data enrichment\" component. In the container world, this would need to run in a separate network namespace, configure AppArmor/SELinux policies, set up egress filtering, etc. With WASM, the component can be composed into the service with zero capabilities, it can compute on the data passed to it and return results. That's it. If the code tries to phone home, the call fails at the WASI boundary, not at a network firewall.\n\n```bash\n# Compose vendor component with NO WASI capabilities\nwasm-tools compose \\\n --definitions vendor-enrichment.wasm \\\n --definitions our-service.wasm \\\n -o final.wasm \\\n orchestrator.wasm\n\n# The vendor component can only:\n# - Receive data through its WIT-defined imports\n# - Return data through its WIT-defined exports\n# - Use CPU and its own linear memory\n# It CANNOT: open files, make network calls, read env vars, spawn processes\n```"},{"heading":"Common Mistakes to Avoid","level":2,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production#common-mistakes-to-avoid","content":"**Mistake 1: Putting JSON parsing at the component boundary.** A common first design has components exchange JSON strings. This meant every cross-component call paid for serialize + deserialize + two string allocations. Switching to WIT ` record ` types with the canonical ABI eliminated this entirely.\n\n**Mistake 2: One giant component instead of composition.** A common initial approach compiles everything into a single WASM module (not component). This worked but lost all the sandboxing benefits and made updates impossible without redeploying everything. The Component Model's composition is the whole point, it lets you update one component without touching others.\n\n**Mistake 3: Ignoring the adapter version.** Three days can be lost debugging a segfault that turned out to be a mismatch between the WASI preview 1 adapter and a component built for preview 2. Pin your adapter versions. Seriously."},{"heading":"When to Use This (and When Not To)","level":2,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production#when-to-use-this-and-when-not-to","content":"Use the WASM Component Model when:\n\n- You need to compose code from multiple languages in a single process\n- Security isolation between components is important\n- You want high deployment density (many services, limited memory)\n- Your services are compute-heavy with small data exchanges\n\nDon't use it when:\n\n- All your code is one language (just use that language's native tooling)\n- You need to exchange large datasets between components (the copy overhead adds up)\n- Your team isn't comfortable with the build toolchain complexity\n- You need mature debugging/profiling tools (they're getting better, but not there yet)\n\nThe Component Model isn't replacing Kubernetes or Docker. It's a new tool for a specific class of problems, and for those problems, it's remarkably effective."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production#questions","content":""},{"heading":"Is the WASM Component Model ready for production use?","level":3,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production#question-1","content":"For specific use cases, yes. We've been running Rust WASM components on Cloudflare Workers and in a custom wasmtime-based orchestrator since Q3 2025. The Component Model spec reached 1.0 in mid-2025, and tooling like wasm-tools 1.2+ and cargo-component 0.16+ is stable. The gap is in ecosystem maturity, most libraries don't expose WIT interfaces yet."},{"heading":"How does cross-language composition actually work?","level":3,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production#question-2","content":"Each component is a self-contained .wasm binary with typed imports and exports defined in WIT. A Go component can import a function exported by a Rust component, and the runtime (wasmtime, wamr, etc.) handles the ABI translation. The key constraint is that components don't share memory, all data crossing boundaries is copied through the canonical ABI."},{"heading":"What's the performance overhead compared to native microservices?","level":3,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production#question-3","content":"For CPU-bound logic, benchmarks show 1.2-1.8x overhead vs native Rust, mostly from bounds checking and indirect calls. For I/O-bound services (which is most real-world code), the overhead is negligible because you're waiting on network/disk anyway. The win is in deployment density, 40 WASM components can run in the memory footprint of 8 native containers."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/rust-wasm-component-model-production#article-tips","content":"1. **Pin your WIT versions in a git submodule:** WIT interfaces are your API contracts. Put them in a dedicated repo and reference as a submodule. When you run ` wasm-tools component new `, pass ` --adapt wasi_snapshot_preview1=adapter.wasm ` to pin the WASI adapter version too. Version drift between WIT and adapter causes silent ABI mismatches.\n2. **Use ` cargo component build --release ` with LTO:** The default debug build produces .wasm files 5-10x larger. Adding ` lto = true ` and ` opt-level = 's' ` in your Cargo.toml release profile, combined with ` wasm-opt -Oz `, typically gets a component from 2MB to 180KB.\n3. **Avoid ` String ` in WIT interface boundaries:** Every string crossing the component boundary requires an allocation + copy because components have separate linear memories. Use ` list<u8> ` with a known encoding, or better, define ` record ` types with fixed-size fields. Production data shows a 40% latency reduction on a JSON-heavy interface by switching to structured records.\n4. **Test component linking locally with ` wasmtime compose `:** Before deploying, run ` wasm-tools compose -d deps/ -o linked.wasm your-component.wasm ` to catch interface mismatches at build time. The error messages from runtime linking are cryptic; the compose tool gives you clear diagnostics.\n\n- rust\n- webassembly\n- component-model\n- wit\n- wasm-tools\n- microservices\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/rust-wasm-component-model-production"},{"text":"WASM Component","url":"https://component-model.bytecodealliance.org/"},{"text":"Cloudflare","url":"https://cloudflare.com/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/rust-wasm-component-model-production#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/rust-wasm-component-model-production"},"headline":"Rust + WASM Component Model in Production: Polyglot Microservices That Actually Work","description":"Building production polyglot microservices with Rust, the WASM Component Model, and WIT interfaces. Cross-language composition, wasm-tools workflows, and hard lessons from running this at scale.","datePublished":"2025-10-08","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/blog/rust-wasm-component-model-production","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"WebAssembly","keywords":"rust, webassembly, component-model, wit, wasm-tools, microservices","wordCount":1462,"timeRequired":"PT8M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/rust-wasm-component-model-production"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"Rust + WASM Component Model in Production: Polyglot Microservices That Actually Work","item":"https://lucioduran.com/blog/rust-wasm-component-model-production"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Is the WASM Component Model ready for production use?","acceptedAnswer":{"@type":"Answer","text":"For specific use cases, yes. We've been running Rust WASM components on Cloudflare Workers and in a custom wasmtime-based orchestrator since Q3 2025. The Component Model spec reached 1.0 in mid-2025, and tooling like wasm-tools 1.2+ and cargo-component 0.16+ is stable. The gap is in ecosystem maturity, most libraries don't expose WIT interfaces yet."}},{"@type":"Question","name":"How does cross-language composition actually work?","acceptedAnswer":{"@type":"Answer","text":"Each component is a self-contained .wasm binary with typed imports and exports defined in WIT. A Go component can import a function exported by a Rust component, and the runtime (wasmtime, wamr, etc.) handles the ABI translation. The key constraint is that components don't share memory, all data crossing boundaries is copied through the canonical ABI."}},{"@type":"Question","name":"What's the performance overhead compared to native microservices?","acceptedAnswer":{"@type":"Answer","text":"For CPU-bound logic, benchmarks show 1.2-1.8x overhead vs native Rust, mostly from bounds checking and indirect calls. For I/O-bound services (which is most real-world code), the overhead is negligible because you're waiting on network/disk anyway. The win is in deployment density, 40 WASM components can run in the memory footprint of 8 native containers."}}]}]}],"contentHash":"3127a0b6cb6cdf1f27e60c18b54d1ba2c990098844971ea714d352b8533c2d1b","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/rust-wasm-component-model-production","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":69005,"contentBytes":14358}}
