<!-- Canonical: https://lucioduran.com/es/blog/rust-wasm-component-model-production
Language: es
Content-SHA256: fa36701f275b1994a731369840dd346278f1065cdcbcb0ca5ab5a9f1ea6aedcb
-->

WebAssembly

# Rust + WASM Component Model en Producción: Microservicios Políglotas Que Realmente Funcionan

2025-10-088 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/rust-wasm-component-model-production>)

## Definición del Component Model

Cortemos con el marketing. Un WASM Component es:

1. Un binario ` .wasm ` que sigue la spec del Component Model (no un módulo wasm "core")
2. Declara **imports** tipados (lo que necesita) y **exports** (lo que provee)
3. Estos tipos se definen en archivos **WIT** (WebAssembly Interface Type)
4. Los componentes se pueden **componer**, conectar en sus boundaries de import/export
5. Cada componente tiene su propia memoria lineal (sandboxeado por default)

Acá va una definición WIT mínima para un componente de autenticación:

```wit
// auth.wit
package myorg:auth@1.0.0;

interface authenticate {
 record auth-request {
 token: string,
 required-scopes: list<string>,
 }

 record auth-result {
 authenticated: bool,
 user-id: option<string>,
 granted-scopes: list<string>,
 error: option<string>,
 }

 verify: func(req: auth-request) -> auth-result;
}

world auth-component {
 export authenticate;
}
```

Y la implementación en Rust:

```rust
// src/lib.rs
wit_bindgen::generate!({
 world: "auth-component",
 path: "../wit/auth.wit",
});

struct AuthComponent;

impl Guest for AuthComponent {
 fn verify(req: AuthRequest) -> AuthResult {
 let claims = match decode_jwt(&req.token) {
 Ok(c) => c,
 Err(e) => return AuthResult {
 authenticated: false,
 user_id: None,
 granted_scopes: vec![],
 error: Some(format!("JWT decode failed: {e}")),
 },
 };

 let granted: Vec<String> = req.required_scopes
 .iter()
 .filter(|s| claims.scopes.contains(s))
 .cloned()
 .collect();

 let all_granted = granted.len() == req.required_scopes.len();

 AuthResult {
 authenticated: all_granted,
 user_id: Some(claims.sub.clone()),
 granted_scopes: granted,
 error: if all_granted { None } else {
 Some("Insufficient scopes".into())
 },
 }
 }
}

export!(AuthComponent);
```

La macro ` wit_bindgen::generate! ` lee el archivo WIT y genera todas las definiciones de tipos y el código glue. Se implementa el trait, se exporta el struct, lo buildeas:

```bash
cargo component build --release
```

Eso produce un componente ` .wasm ` (no un módulo core) que cualquier runtime del Component Model puede cargar y llamar.

## Composición Cross-Language: El Poder Real

Acá resulta interesante. Supongamos que tu componente de auth está en Rust, pero tu API gateway está en Go, y tu lógica de negocio está en Python (usando componentize-py). Cada uno se compila a un componente WASM separado.

Primero, el WIT del componente gateway en Go:

```wit
// gateway.wit
package myorg:gateway@1.0.0;

interface handle-request {
 record http-request {
 method: string,
 path: string,
 headers: list<tuple<string, string>>,
 body: option<list<u8>>,
 }

 record http-response {
 status: u16,
 headers: list<tuple<string, string>>,
 body: list<u8>,
 }

 handle: func(req: http-request) -> http-response;
}

world gateway-component {
 import myorg:auth/authenticate@1.0.0;
 import myorg:bizlogic/process@1.0.0;
 export handle-request;
}
```

La implementación en Go importa la interfaz de auth y la llama como una función local, sin HTTP, sin serialización:

```go
package main

import (
 "myorg/gateway/gen"
 auth "myorg/auth/gen"
 bizlogic "myorg/bizlogic/gen"
)

type Gateway struct{}

func (g *Gateway) Handle(req gen.HttpRequest) gen.HttpResponse {
 token := extractBearerToken(req.Headers)

 // Llamar al componente auth de Rust, mismo proceso, sin red
 authResult := auth.Verify(auth.AuthRequest{
 Token: token,
 RequiredScopes: []string{"read:users"},
 })

 if !authResult.Authenticated {
 return gen.HttpResponse{
 Status: 401,
 Body: []byte(`{"error": "unauthorized"}`),
 }
 }

 // Llamar al componente de lógica de negocio en Python
 result := bizlogic.Process(bizlogic.ProcessRequest{
 UserId: *authResult.UserId,
 Action: "list_users",
 Payload: req.Body,
 })

 return gen.HttpResponse{
 Status: 200,
 Headers: []gen.Header{{"content-type", "application/json"}},
 Body: result.Data,
 }
}
```

Ahora los componemos:

```bash
# Buildear cada componente
cargo component build --release -p auth-component
tinygo build -target=wasip2 -o gateway.wasm ./gateway
componentize-py -d ./wit -w bizlogic-component ./bizlogic.py -o bizlogic.wasm

# Componer en un solo componente linkeado
wasm-tools compose \
 --definitions auth-component.wasm \
 --definitions bizlogic.wasm \
 -o composed-service.wasm \
 gateway.wasm
```

El ` composed-service.wasm ` es un solo binario conteniendo los tres componentes, con sus imports/exports conectados. Lo es posible correr en wasmtime, [Cloudflare](<https://cloudflare.com/>) Workers, Fermyon Spin, o cualquier runtime del Component Model.

## El Pipeline de Build en Detalle

Hacer funcionar esto en CI toma más esfuerzo del esperado. Acá está un Makefile que realmente funciona:

```makefile
WIT_DIR := ./wit
BUILD_DIR := ./build
WASI_ADAPTER := ./adapters/wasi_snapshot_preview1.reactor.wasm

.PHONY: all clean validate compose

all: validate compose

validate:
	wasm-tools component wit $(WIT_DIR) --check

$(BUILD_DIR)/auth.wasm: auth/src/lib.rs $(WIT_DIR)/auth.wit
	cd auth && cargo component build --release
	cp auth/target/wasm32-wasip2/release/auth_component.wasm $@
	wasm-opt -Oz $@ -o $@

$(BUILD_DIR)/gateway.wasm: gateway/main.go $(WIT_DIR)/gateway.wit
	cd gateway && tinygo build -target=wasip2 -o ../$@
	wasm-tools component new $@ --adapt $(WASI_ADAPTER) -o $@

$(BUILD_DIR)/bizlogic.wasm: bizlogic/app.py $(WIT_DIR)/bizlogic.wit
	componentize-py -d $(WIT_DIR) -w bizlogic-component bizlogic/app.py -o $@

compose: $(BUILD_DIR)/auth.wasm $(BUILD_DIR)/gateway.wasm $(BUILD_DIR)/bizlogic.wasm
	wasm-tools compose \
		-d $(BUILD_DIR)/auth.wasm \
		-d $(BUILD_DIR)/bizlogic.wasm \
		-o $(BUILD_DIR)/composed-service.wasm \
		$(BUILD_DIR)/gateway.wasm
	@echo "Binario compuesto: $$(du -h $(BUILD_DIR)/composed-service.wasm | cut -f1)"

clean:
	rm -rf $(BUILD_DIR)/*
```

Dos lecciones importantes:

1. **La validación de WIT tiene que ir primero.** Un typo en un archivo WIT produce errores inescrutables downstream. ` wasm-tools component wit --check ` los atrapa temprano.
2. **El adapter WASI es sensible a la versión.** TinyGo, el target ` wasm32-wasip2 ` de Rust, y componentize-py cada uno espera una versión específica de WASI preview. El binario del adapter se debe pinar en el repo y referenciarlo explícitamente.

## Rendimiento: Análisis de Compromisos

Acá van números reales de un deployment de producción. El setup tiene 12 componentes WASM (mezcla de Rust, Go y Python) compuestos en 3 servicios, corriendo en un orquestador custom basado en wasmtime 18 en bare metal.

**Overhead de llamada entre componentes:**

| Tamaño del payload | Latencia | Notas |
| --- | --- | --- |
| Vacío (sin args) | 0.8μs | Costo base de dispatch |
| Struct de 1KB | 2.1μs | Incluye serialización ABI canónica |
| Blob de 64KB | 18μs | Dominado por memcpy entre memorias lineales |
| Blob de 1MB | 156μs | Lineal con el tamaño, como se espera |

Para contexto, una llamada gRPC por loopback es ~50μs para un mensaje vacío, y ~200μs para 1KB. Así que las llamadas entre componentes son ~25x más rápidas que gRPC para payloads chicos. La brecha se achica con payloads grandes porque ambos están bottlenecked en la copia de datos.

**Overhead de CPU vs nativo (benchmark compute-bound, sin I/O):**

| Lenguaje del componente | vs nativo | Notas |
| --- | --- | --- |
| Rust → WASM | 1.15x | Overhead mínimo, bounds checks |
| Go → WASM (TinyGo) | 1.45x | El GC de TinyGo es más simple, faltan algunas opts |
| Python → WASM | 3.2x | componentize-py incluye intérprete CPython |

El número de Python se ve feo, pero recordar: este componente Python corre en el mismo proceso, sin overhead de container, sin cold start, y con sandboxing completo. Si la alternativa es un container Python con 800ms de cold start, 3.2x de overhead de CPU en una computación de 5ms es un trade fantástico.

**Huella de memoria:**

| Deployment | Memoria por instancia |
| --- | --- |
| 12 containers Docker (1 por componente) | ~3.2GB total |
| 3 servicios WASM compuestos (4 componentes c/u) | ~420MB total |
| Un solo WASM compuesto (los 12 componentes) | ~180MB |

Los ahorros de memoria son dramáticos porque los componentes WASM comparten el runtime (wasmtime) y solo alocan su propia memoria lineal. La memoria lineal de un componente Rust empieza en 64KB y crece bajo demanda.

## La Historia del Sandboxing

Esta es la parte que convence a los equipos de seguridad. Cada componente WASM:

- No puede acceder al filesystem a menos que se le dé explícitamente una capability ` wasi:filesystem `
- No puede hacer llamadas de red a menos que se le dé ` wasi:sockets `
- No puede leer la memoria de otro componente (memorias lineales separadas)
- No puede ejecutar syscalls arbitrarios (modelo de capabilities de WASI)

Consideremos un vendor tercero que provee un componente de "enrichment de datos". En el mundo de containers, se necesitaría correrlo en un namespace de red separado, configurar políticas AppArmor/SELinux, armar filtrado de egress, etc. Con WASM, el componente se puede componer en el servicio con cero capabilities, puede computar sobre los datos que se le pasan y devolver resultados. Eso nomás. Si el código intenta llamar a casa, la llamada falla en el boundary de WASI, no en un firewall de red.

```bash
# Componer componente del vendor SIN capabilities WASI
wasm-tools compose \
 --definitions vendor-enrichment.wasm \
 --definitions our-service.wasm \
 -o final.wasm \
 orchestrator.wasm
```

## Errores Comunes a Evitar

**Error 1: Poner parsing de JSON en el boundary del componente.** Un primer diseño común tiene componentes intercambiando strings JSON. Esto significaba que cada llamada cross-componente pagaba por serialize + deserialize + dos alocaciones de string. Cambiar a tipos ` record ` de WIT con el ABI canónico eliminó esto completamente.

**Error 2: Un componente gigante en vez de composición.** Un enfoque inicial común compila todo en un solo módulo WASM (no componente). Funcionaba pero perdía todos los beneficios de sandboxing y hacía imposible actualizar sin redeployar todo. La composición del Component Model es el punto, te deja actualizar un componente sin tocar los otros.

**Error 3: Ignorar la versión del adapter.** Es posible perder tres días debuggeando un segfault que resultó ser un mismatch entre el adapter WASI preview 1 y un componente buildeado para preview 2. Fijar las versiones de tu adapter. En serio.

## Cuándo Usar Esto (y Cuándo No)

Utilizar el WASM Component Model cuando:

- Se necesitan componer código de múltiples lenguajes en un solo proceso
- El aislamiento de seguridad entre componentes es importante
- Querés alta densidad de deployment (muchos servicios, memoria limitada)
- Tus servicios son compute-heavy con intercambios de datos chicos

No lo uses cuando:

- Todo tu código es un solo lenguaje (utilizar el tooling nativo de ese lenguaje)
- Se necesitan intercambiar datasets grandes entre componentes (el overhead de copia se acumula)
- Tu equipo no se siente cómodo con la complejidad del build toolchain
- Se necesitan herramientas maduras de debugging/profiling (están mejorando, pero no llegaron todavía)

El Component Model no está reemplazando Kubernetes ni Docker. Es una herramienta nueva para una clase específica de problemas, y para esos problemas, es notablemente efectivo.

## Preguntas frecuentes

### ¿El WASM Component Model está listo para producción?

Para case es de uso específicos, sí. Se vienen corriendo componentes WASM de Rust en Cloudflare Workers y en un orquestador custom basado en wasmtime desde Q3 2025. La spec del Component Model llegó a 1.0 a mediados de 2025, y herramientas como wasm-tools 1.2+ y cargo-component 0.16+ son estables. La brecha está en la madurez del ecosistema, la mayoría de las librerías todavía no exponen interfaces WIT.

### ¿Cómo funciona realmente la composición cross-language?

Cada componente es un binario .wasm autocontenido con imports y exports tipados definidos en WIT. Un componente Go puede importar una función exportada por un componente Rust, y el runtime (wasmtime, wamr, etc.) maneja la traducción ABI. La restricción clave es que los componentes no comparten memoria, todos los datos que cruzan boundaries se copian a través del ABI canónico.

### ¿Cuál es el overhead de rendimiento comparado con microservicios nativos?

Para lógica CPU-bound, los benchmarks muestran 1.2-1.8x de overhead vs Rust nativo, mayormente por bounds checking y llamadas indirectas. Para servicios I/O-bound (que es la mayoría del código real), el overhead es despreciable porque igual se está esperando red/disco. La ganancia está en la densidad de deployment, 40 componentes WASM pueden correr en la huella de memoria de 8 containers nativos.

## Tips

1. **Fijar tus versiones de WIT en un git submodule:** Las interfaces WIT son tus contratos de API. Ponelas en un repo dedicado y referenciá como submodule. Cuando corrés ` wasm-tools component new `, pasá ` --adapt wasi_snapshot_preview1=adapter.wasm ` para pinar la versión del adapter WASI también. El drift de versiones entre WIT y adapter causa mismatches de ABI silenciosos.
2. **Utilizar ` cargo component build --release ` con LTO:** El build debug default produce archivos .wasm 5-10x más grandes. Agregar ` lto = true ` y ` opt-level = 's' ` en tu perfil release del Cargo.toml, combinado con ` wasm-opt -Oz `, típicamente lleva un componente de 2MB a 180KB.
3. **Evitar ` String ` en los límites de interfaz WIT:** Cada string que cruza el límite del componente requiere una alocación + copia porque los componentes tienen memorias lineales separadas. Utilizar ` list<u8> ` con encoding conocido, o mejor, definí tipos ` record ` con campos de tamaño fijo. Los datos de producción muestran una reducción de latencia del 40% en una interfaz pesada de JSON al cambiar a records estructurados.
4. **Probar el linking de componentes localmente con ` wasmtime compose `:** Antes de deployar, ejecutar ` wasm-tools compose -d deps/ -o linked.wasm your-component.wasm ` para atrapar mismatches de interfaz en build time. Los mensajes de error del linking en runtime son crípticos; la herramienta compose te da diagnósticos claros.

- rust
- webassembly
- component-model
- wit
- wasm-tools
- microservicios

[Volver al Blog](<https://lucioduran.com/es/blog>)
