moxzi
Docs / Actor platforms / Proto.Actor

Proto.Actor

Proto.Actor (Go) was the reference host the actor-host contract, version 2, was proven on first — its orchestration now lives in a shared Go kit (moxzi/adapters/hostkit-go), and Orleans, Pekko and Cloudflare each reuse the same design in their own platform's terms. The code is moxzi/adapters/protoactor (moxzi-protoactor).

What Proto.Actor owns, what Motoko owns#

OwnsWhoWhat
IdentityProto.Actor clusterone virtual actor per key (kind MoxziActor, identity Type/id); the cluster places it, and its mailbox is the one-step-at-a-time guarantee a turn needs
Routingthis process, beside the virtual actorevery parked await becomes a durable route row (a Postgres _routes table) before it is routed; the callee's turn runs as a turn of its own; a restart re-drives every non-held route
Durable timersthis process, in Postgresa _timers table polled every 200 ms; a claimed row (DELETE … RETURNING) fires exactly once
Placement / held / pending callsthis process, in Postgres_placement, _held tables behind PUT /_moxzi/placement and POST /_moxzi/hold
Motoko execution, the heap, upgradesmoxzi-actor-host (Rust)everything past /v2/* — the virtual actor's Receive does exactly one JSON POST per step and nothing more

Timers and routing run beside the virtual actor rather than inside its Receive: a mailbox that blocked on a routed call would hold the key for the whole await, and two actors awaiting each other would deadlock — the IC lets a parked actor keep taking other messages. So the actor does one HTTP step and returns; a Host object (from the shared kit) drives the resulting call chain outside any actor's mailbox.

Proto.Actor has no durable reminder and no built-in store of its own, so — like the Pekko host — both the route journal and the timer table are Postgres, in tables <namespace>_placement, <namespace>_held, <namespace>_routes, <namespace>_timers.

Prerequisites#

Verified on this machine:

Walk through it#

Write and build the two actors#

Same two actors as the contract page — Customer awaits Bank on chargeVia; both are 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 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 (285491 bytes, 1 file(s), 65 steps, 0.83s)
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 moxzi-actor-host services#

moxzi/target/release/moxzi-actor-host --listen 127.0.0.1:3021 --state ./s1 \
  --namespace demo --deployment crm --actor Customer=./customer.wasm --known Bank/1
# moxzi-actor-host: Bank/1 is k2e4o-mgnlm-is6i5-r7owd-3wgr3-b4lrw-vi2fq-vf6zl-43mw6-2jicp-kae
# moxzi-actor-host: moxzi-actor-host/2 on 127.0.0.1:3021, hosting Customer

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

--known Bank/1 just prints the principal early; if you'd rather ask for it, any running service will answer POST /v2/learn {"key":"Bank/1"} with {"principal":"k2e4o-mgnlm-is6i5-r7owd-3wgr3-b4lrw-vi2fq-vf6zl-43mw6-2jicp-kae"} — the same value, because it is derived deterministically from namespace/deployment/type/key.

Start Proto.Actor#

Pick a Postgres host port nothing else on the box is using:

docker run -d --name moxzi-tutorial-pg -e POSTGRES_PASSWORD=moxzi -e POSTGRES_DB=moxzi \
  -p 25433:5432 postgres:16

cd moxzi/adapters/protoactor
go build -o moxzi-protoactor .
DATABASE_URL="postgres://postgres:moxzi@127.0.0.1:25433/moxzi?sslmode=disable" \
  MOXZI_HOST_NS=demo ./moxzi-protoactor --listen 127.0.0.1:3500
# 2026/09/23 20:52:17 moxzi-protoactor: contract moxzi-actor-host/2, cluster member up, listening on 127.0.0.1:3500

Point placement at the services, and call it#

curl -X PUT localhost:3500/_moxzi/placement \
  -d '{"types":{"Customer":["http://127.0.0.1:3021"],"Bank":["http://127.0.0.1:3022"]}}'

curl -X POST localhost:3500/actors/Customer/1/method/addOrder \
  -H 'content-type: application/json' -d '[{"id":1,"total":30}]'
# HTTP/1.1 200 OK, x-moxzi-outcome: replied
# [1]

A cross-actor await#

curl -X POST localhost:3500/actors/Customer/1/method/chargeVia \
  -H 'content-type: application/json' \
  -d '["k2e4o-mgnlm-is6i5-r7owd-3wgr3-b4lrw-vi2fq-vf6zl-43mw6-2jicp-kae",10]'
# [40]
curl -X POST localhost:3500/actors/Bank/1/method/total -H 'content-type: application/json' -d '[]'
# [10]

Customer parked on Bank's answer; the cluster's virtual-actor placement routed the call to whichever service owns Bank/1, and the host resumed Customer with the reply — getBalance now reads 30 + 10 = 40.

Arm a timer#

curl -X POST localhost:3500/actors/Customer/1/method/armTimer \
  -H 'content-type: application/json' -d '[2000000000]'
sleep 3
curl -X POST localhost:3500/actors/Customer/1/method/timerCount -H 'content-type: application/json' -d '[]'
# [1]

Kill it and bring it back#

pkill -f moxzi-protoactor
pkill -f 'moxzi-actor-host --listen 127.0.0.1:302'

# … restart moxzi-protoactor exactly as above, and both moxzi-actor-host services on the
# SAME --state dirs …

curl -X POST localhost:3500/actors/Customer/1/method/getBalance -H 'content-type: application/json' -d '[]'
# [40]  — placement came back from Postgres, no re-PUT needed
curl -X POST localhost:3500/actors/Customer/1/method/timerCount -H 'content-type: application/json' -d '[]'
# [1]

Operating notes#

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

In process#

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

In process on Proto.Actor
Bindingcgo, inside the cluster member
The actor's image lives inPostgres
Update latency vs Proto.Actor's own durable actor (p50, macOS)2.39x (1.41x on Linux)
ConformanceHOST=protoactor-inproc scripts/host_matrix_gate.sh

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

(cd ../../ && cargo build --release -p moxzi-rts)   # libmoxzi_rts first
go build -tags inproc -o out/moxzi-protoactor .
DATABASE_URL=… MOXZI_IN_PROCESS=1 out/moxzi-protoactor --listen 127.0.0.1:8090

The conformance matrix#

Proto.Actor's column of the spec §61 matrix, real host, real process kills (docs/host-adapters.md, measured 2026-09-23 against compiler b7577666):

§61 scenarioProto.Actor
PersistencePASS
Process FailurePASS
Trap RollbackPASS
Actor IsolationPASS
Actor ParallelismPASS 1.04×
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

Reproduce it (builds the Go binary, starts a dedicated Postgres container, the Proto.Actor member, and two moxzi-actor-host instances per actor type, then drives all sixteen scenarios with real kills and restarts):

HOST=protoactor scripts/host_matrix_gate.sh
On this pageWhat Proto.Actor owns, what Motoko ownsPrerequisitesWalk through itWrite and build the two actorsStart the moxzi-actor-host servicesStart Proto.ActorPoint placement at the services, and call itA cross-actor awaitArm a timerKill it and bring it backOperating notesIn processThe conformance matrix