moxzi
Docs / Actor platforms / wasmCloud

wasmCloud

wasmCloud has no actor primitive of its own, so this host is built from wasmCloud's own pieces: a stateless component (wasi:http/incoming-handler, a fresh instance per request, no memory between calls) is the whole host-control HTTP surface, and a persistent service (wasi:cli/run, started once, expected to block forever) is the durable timer loop. Both share a filesystem-backed wasi:keyvalue bucket — confirmed durable across a kill -9, below — for placement, held calls, the route journal and armed timers, since nothing a stateless component touches survives between invocations otherwise. The code is moxzi/adapters/wasmcloud (Rust, wasm32-wasip2, wash 2.5.2), built by moxzi/conformance/hosts/wasmcloud.json and driven by HOST=wasmcloud scripts/host_matrix_gate.sh.

What wasmCloud owns, what Motoko owns#

OwnsWhoWhat
The Wasm execution sandboxwasmCloud (wash-runtime)both workloads; wasi:http ingress (binds :8000 — see below) and outgoing-handler (gated by workload.allowedHosts); the in-process wasi:keyvalue host plugin; component/service lifecycle
Per-key identity and placementthis adapter (api/)FNV-1a32 over Type/id, matching the conformance harness's own hash
One-at-a-time execution per keythis adapter (api/)a wasi:keyvalue/atomics.increment ticket lock — wasmCloud gives a component no mailbox to lean on, unlike Dapr's actor turn or Proto.Actor's virtual actor
The route journal, held/pending/deliver/cancelthis adapter (api/)the same orchestration as hostkit.go, ported to Rust, state in the shared KV bucket instead of a Go struct
The durable timerthis adapter (timer/, the persistent service)nothing invokes a stateless component on a schedule, so a service polls timer:* and runs one locked /v2/remind step per hit
Motoko execution, the heap, upgradesmoxzi-actor-host (Rust, native, outside wasmCloud)everything past /v2/* — this adapter calls it over HTTP and never receives a callback

The per-key lock is a ticket lock, not a mutex: wasi:keyvalue/atomics has one atomic primitive, increment (no compare-and-swap), so acquiring is "increment lock:{key} and check I landed on 1", releasing is deleting the key. A holder that dies mid-turn would wedge that key forever — there is no lease/expiry — so timer/ sweeps every lock:* at boot, before flipping ready on (which gates api/'s /healthz): a fresh process can never still be holding a lock from its own previous life, so anything left over is provably orphaned.

Prerequisites#

Verified on this machine:

Walk through it#

Only one wasmCloud walk-through (or gate run) at a time on this machine: wash dev's HTTP ingress binds 0.0.0.0:8000 unconditionally in this version — no flag, env var, or .wash/config.yaml key moves it (checked empirically). /tmp/moxzi-tutorial-wasmcloud/ is the scratch directory for this walk-through, and run.sh refuses to start if :8000 is already held by something it did not start.

Write and build the two actors#

Same two actors as every other host page — Customer awaits Bank on chargeVia; both simplified from moxzi/conformance/fixtures/{customer,bank}.mo:

// customer.mo
import Prim "mo:⛔";
persistent actor {
  type Order = { id : Nat; total : Nat };
  var balance : Nat = 0;
  var orders : [Order] = [];
  var timerFired : Nat = 0;

  public func addOrder(order : Order) : async Nat {
    orders := Prim.Array_tabulate<Order>(orders.size() + 1, func (i : Nat) : Order {
      if (i < orders.size()) orders[i] else order
    });
    balance += order.total;
    orders.size()
  };
  public query func getBalance() : async Nat { balance };
  public query func orderCount() : async Nat { orders.size() };

  public func chargeVia(bank : Text, amount : Nat) : async Nat {
    let b : actor { debit : (Nat) -> async Nat } = actor (bank);
    let v = await b.debit(amount);
    balance += v;
    balance
  };

  public func armTimer(delayNs : Nat64) : async () {
    ignore Prim.setTimer<system>(delayNs, false, func () : async () { timerFired += 1 });
  };
  public query func timerCount() : async Nat { timerFired };
};
// bank.mo
persistent actor {
  var debited : Nat = 0;
  public func debit(n : Nat) : async Nat { debited += n; n };
  public query func total() : async Nat { debited };
};
MOXZI_COMPILER=bin/compiler-canister/compiler.wasm MOXZI_LINKER=bin/linker-canister/linker.wasm \
  moxzi/target/release/moxzi build customer.mo -o customer.wasm
# customer.mo -> customer.wasm + customer.did (272261 bytes, 1 file(s), 65 steps, 0.79s)
MOXZI_COMPILER=bin/compiler-canister/compiler.wasm MOXZI_LINKER=bin/linker-canister/linker.wasm \
  moxzi/target/release/moxzi build bank.mo -o bank.wasm

Start the wasmCloud host#

run.sh PORT OUT fronts wash dev's fixed :8000 with a dependency-free byte relay bound to PORT, and points .wash/config.yaml's wasi_keyvalue_path at OUT/kv-store — the same launch the conformance harness uses (moxzi/conformance/hosts/wasmcloud.json):

cd moxzi/adapters/wasmcloud
rustup target add wasm32-wasip2   # once
bash run.sh 29700 /tmp/moxzi-tutorial-wasmcloud/out &
# INFO HTTP server listening addr=0.0.0.0:8000 protocol="HTTP"
# portproxy: 127.0.0.1:29700 -> 127.0.0.1:8000
# INFO listening for HTTP requests address=http://127.0.0.1:8000
# moxzi-wasmcloud-timer: ready, polling every 250ms

curl -s http://127.0.0.1:29700/healthz
# ok

Start the moxzi-actor-host services#

moxzi/target/release/moxzi-actor-host --listen 127.0.0.1:29721 --state ./state-customer \
  --namespace tutorial --deployment crm --actor Customer=./customer.wasm --known Bank/1
# moxzi-actor-host: Bank/1 is 6lkaw-mxxfa-ayudl-lyjba-qiou3-hjvqo-deil4-f3r6l-apodg-jbfpw-uae

moxzi/target/release/moxzi-actor-host --listen 127.0.0.1:29722 --state ./state-bank \
  --namespace tutorial --deployment crm --actor Bank=./bank.wasm

Point placement at the services, and call it#

curl -X PUT http://127.0.0.1:29700/_moxzi/placement -H 'content-type: application/json' \
  -d '{"types":{"Customer":["http://127.0.0.1:29721"],"Bank":["http://127.0.0.1:29722"]}}'
# HTTP 204

curl -X POST http://127.0.0.1:29700/actors/Customer/1/method/addOrder \
  -H 'content-type: application/json' -d '[{"id":1,"total":30}]'
# [1]  HTTP 200

A cross-actor await#

curl -X POST http://127.0.0.1:29700/actors/Customer/1/method/chargeVia \
  -H 'content-type: application/json' \
  -d '["6lkaw-mxxfa-ayudl-lyjba-qiou3-hjvqo-deil4-f3r6l-apodg-jbfpw-uae", 5]'
# [35]  HTTP 200

curl -X POST http://127.0.0.1:29700/actors/Bank/1/method/total -H 'content-type: application/json' -d '[]'
# [5]

getBalance now reads 30 + 5 = 35: api/ routed the call through its own journal in the shared KV bucket, exactly the "route journal written before routing" pattern every host in the matrix uses.

Arm a timer, and watch it fire#

curl -X POST http://127.0.0.1:29700/actors/Customer/1/method/armTimer \
  -H 'content-type: application/json' -d '[3000000000]'
sleep 6
curl -X POST http://127.0.0.1:29700/actors/Customer/1/method/timerCount -H 'content-type: application/json' -d '[]'
# [1]

timer/, the one persistent process in this deployment, polled timer:* and ran one locked /v2/remind step once the due row appeared.

Kill it and bring it back#

Service-level restart — the Motoko heap is a file on disk, independent of wasmCloud:

kill <moxzi-actor-host Customer pid>          # 29721
moxzi/target/release/moxzi-actor-host --listen 127.0.0.1:29721 --state ./state-customer \
  --namespace tutorial --deployment crm --actor Customer=./customer.wasm --known Bank/1

curl -X POST http://127.0.0.1:29700/actors/Customer/1/method/getBalance -H 'content-type: application/json' -d '[]'
# [35]
curl -X POST http://127.0.0.1:29700/actors/Customer/1/method/timerCount -H 'content-type: application/json' -d '[]'
# [1]

Host-level restart — kill -9 the whole run.sh tree (wash dev and the port relay together), confirming the ports really free up, then bring it back on the same OUT directory so it points at the same KV bucket:

kill -9 <run.sh pid> <wash dev pid> <portproxy.py pid>
sleep 2
nc -z 127.0.0.1 8000 || echo "8000 free"
nc -z 127.0.0.1 29700 || echo "29700 free"

cd moxzi/adapters/wasmcloud
bash run.sh 29700 /tmp/moxzi-tutorial-wasmcloud/out &
curl -s http://127.0.0.1:29700/healthz
# ok

curl -X POST http://127.0.0.1:29700/actors/Customer/1/method/getBalance -H 'content-type: application/json' -d '[]'
# [35]  — no re-PUT of placement: the filesystem-backed wasi:keyvalue bucket survived the kill -9
curl -X POST http://127.0.0.1:29700/actors/Customer/1/method/timerCount -H 'content-type: application/json' -d '[]'
# [1]

Drain, then upgrade#

curl -X POST http://127.0.0.1:29700/actors/Customer/1/admin/drain -d '{}'
# {"state":"Quiescent"}
curl -X POST http://127.0.0.1:29700/actors/Customer/1/admin/upgrade \
  -H 'content-type: application/json' \
  -d "{\"deploymentId\":\"crm-v2\",\"wasm\":\"$(base64 -i customer.wasm | tr -d '\n')\"}"
# {"state":"Active"}
curl -X POST http://127.0.0.1:29700/actors/Customer/1/method/getBalance -H 'content-type: application/json' -d '[]'
# [35]

Operating notes#

These notes are for the service form. In process, the actor's image lives in the same wasi:keyvalue bucket and nothing is written to a service's disk — see In process below.

In process#

The same actor also runs inside wasmCloud's own process through moxzi-rts, with wasmCloud keeping its image: see /docs/hosts/in-process/ for the contract.

In process on wasmCloud
Bindinga wash-runtime embedder with a moxzi plugin
The actor's image lives inthe same filesystem-backed wasi:keyvalue bucket as the service form
Update latency vs wasmCloud's own durable actor (p50, macOS)4.39x
ConformanceHOST=wasmcloud-inproc scripts/host_matrix_gate.sh

The ratio is high because wasmCloud's own durable counter is fast (0.31 ms), not because the Motoko turn is slow.

From moxzi/adapters/wasmcloud/ (the exact commands and environment are in moxzi/conformance/hosts/wasmcloud-inproc.json):

cargo build --workspace --target wasm32-wasip2 --release --features in_process
(cd host && cargo build --release)
host/target/release/moxzi-wasmcloud-inproc-host ./kv-store

The conformance matrix#

wasmCloud's column of the spec §61 matrix, real wash-runtime, real process kills (moxzi/adapters/wasmcloud/README.md, measured 2026-09-23 against compiler b7577666):

§61 scenariowasmCloud
PersistencePASS
Process FailurePASS
Trap RollbackPASS
Actor IsolationPASS
Actor ParallelismPASS 1.05–1.06×
Duplicate InvocationPASS
AwaitPASS
Await Across RestartPASS
Upgrade With No AwaitsPASS
Upgrade With Outstanding AwaitPASS
Permanently Unresolved AwaitPASS
Timer Across PassivationPASS
Timer Across Process FailurePASS
Tiny Heap Mutation (206 B on an 8 MB heap)PASS
ICRC-72PASS
Upgrade CompatibilityPASS
total16/16

Run twice: both 16/16, no scenario needed a SKIP (Actor Parallelism confirms two placements really run concurrently — wasmCloud schedules concurrent component invocations natively). Both runs left no wash, wash-runtime, moxzi-actor-host, or portproxy.py process behind, and used no Docker container — this adapter needs none.

Reproduce it (builds the Rust workspace for wasm32-wasip2, starts both workloads and two moxzi-actor-host instances per actor type, then drives all sixteen scenarios with real kills and restarts; never touches Docker):

HOST=wasmcloud scripts/host_matrix_gate.sh
On this pageWhat wasmCloud owns, what Motoko ownsPrerequisitesWalk through itWrite and build the two actorsStart the wasmCloud hostStart the moxzi-actor-host servicesPoint placement at the services, and call itA cross-actor awaitArm a timer, and watch it fireKill it and bring it backDrain, then upgradeOperating notesIn processThe conformance matrix