Tour
The programs in examples/ are the tour of nodeakt. Each one is small, self-contained, and exists to show one thing the runtime does. Read them in the order below; later examples assume the earlier ones.
They run under tsx and import the library from src/, so there is no build step. From a clone of the repository:
make # list the examples
make helloworld # run oneOr without make:
pnpm example examples/helloworld/main.tsActors own their state: helloworld
An actor processes messages one at a time. Its state is private and is only ever mutated inside receive, so it needs no lock. tell is fire and forget; ask waits for ctx.response.
helloworld/main.ts, make helloworld
Hello, Ada!
Hello, Alan!
Greeted 2 people.An actor is a state machine: behaviors
become swaps the message handler at runtime. State lives in which behavior is active, not in a pile of flags. unBecome reverts to the default receive.
behaviors/main.ts, make behaviors
🔴 red -> 🟢 green
🟢 green -> 🟡 yellow
🟡 yellow -> 🔴 red
…Defer work until ready: stash
Requests that arrive before an actor has finished initializing are stashed. When it is ready, become plus unstashAll replays them in arrival order to the new behavior. No lock, no busy wait.
stash/main.ts, make stash
stashing "a" (not ready yet)
stashing "b" (not ready yet)
initialized; switching to serving and replaying the backlog
processing "a"
processing "b"Death is a message: watch
watch registers interest in another actor's stop. The runtime delivers Terminated as an ordinary message, handled on the watcher's own turn. No polling, no shared flag, no callback from another thread.
watch/main.ts, make watch
sentinel is watching "worker"
sentinel saw "nodeakt://watch@…/user/worker" terminateMany actors, no shared memory: chat
Actors collaborate only by sending messages. A room fans posts out with tell, learns who joined from ctx.sender, and watches each member so a stop drops them from the room automatically.
chat/main.ts, make chat
* alice joined (1 present)
* bob joined (2 present)
[alice] <alice> hello room
[bob] <alice> hello room
[alice] <bob> hi alice
[bob] <bob> hi alice
* bob left (1 present)
[alice] <alice> anyone still here?Ask without freezing: reentrancy
ctx.ask parks the actor until the reply lands. ctx.request does not: it returns a handle immediately, and onReply runs later on this actor's own turn, serialized with its other messages. The actor keeps serving while requests are in flight.
reentrancy/main.ts, make reentrancy
status while computing: 2 in flight
alpha = 50 (1 still in flight)
bravo = 50 (0 still in flight)Let it crash: supervision
A failure suspends the actor and asks its parent what to do. A Supervisor maps the error to restart with backoff: the runtime re-runs preStart (fresh state) and keeps the jobs that were already queued. Recovery is message-driven; nothing spins or holds a lock.
supervision/main.ts, make supervision
worker started with fresh state
did job 1 (completed 1 since last start)
did job 2 (completed 2 since last start)
worker started with fresh state
did job 3 (completed 1 since last start)
did job 4 (completed 2 since last start)Job 13 throws. The counter resetting to 1 is the restart, not a patch.
A small IoT system: iot
Everything so far, composed, in the shape of the classic Akka IoT tutorial. A device manager routes by group id and builds the hierarchy on demand: one group per home, one actor per sensor, each watched by its group. Reading the whole home is a short-lived actor per query: it fans ReadTemperature out with tell, watches every device so a death becomes an answer, and settles whatever is left when a deadline it scheduleOnced to itself fires.
iot/main.ts, make iot
registered nodeakt://iot@…/device-manager/group-home/device-kitchen
…
group home tracks: kitchen, bedroom, garage, attic
-- query 1: every sensor answers --
query #1 answered:
kitchen 22.5°C
bedroom no reading yet
garage 18.0°C
attic no reading yet
-- query 2: a jammed sensor and a dying one --
group home dropped garage (3 still tracked)
query #2 answered:
kitchen 22.5°C
bedroom 19.2°C
garage device stopped
attic no answer before the deadlineThe second answer is the point: a reading, a sensor with nothing recorded, a death observed mid-query, and a deadline all come back as one uniform reply.
Construction is data: props
spawn takes a live instance (always this core) or a Props, which captures the class and its constructor arguments as data. From that data the runtime can build the actor wherever it places it. The arguments are checked by the compiler against the constructor.
props/main.ts, make props
Bonjour, Ada!
Hola, Alan!
Hello, Grace!This run forces NODEAKT_PARALLELISM=1 so placement stays local. The spawn call is the same as in multicore.
Every core, invisibly: multicore
Start a system, spawn one CPU-bound actor per core with Props, and ask them all. There is no worker, pool, or isolate wiring in the program. Messages that cross cores are registered classes, so instanceof still works on the far side.
multicore/main.ts, make multicore
spawned 8 counters, each on its own core
counter-0: 78498 primes below 1000000
…
parallel (all 8 cores at once): 1200 ms
serial (one core at a time): 6800 ms
speedup: 5.7xCore count, timings, and speedup depend on the machine. Hybrid performance/efficiency cores will not scale linearly; that is honest.
Idle actors reclaim themselves: passivation
A presence server keeps one session actor per online user. Each session holds that user's in-memory state and passivates itself after an idle window, so idle users cost nothing; an active user's session stays put, and a returning user opens a fresh one. This is the pattern for per-entity actors (sessions, connections, device shadows, carts). The default is time-based at two minutes; the example sets a short window so a session comes and goes while you watch.
passivation/main.ts, make passivation
session opened for alice
alice active: 3 actions
still open after staying active: 5 actions
alice goes idle...
passivated: alice was idle, so her session's memory is reclaimed
session closed for alice; 5 action(s) reclaimed
actorOf("session-alice") -> absent
alice returns: 0 actions on a fresh sessionNobody stops the session by hand: it stays alive while active, and the runtime reclaims it once idle. postStop runs on passivation too, the place to flush before the state is gone. A returning user gets a new instance with reset state, so passivation is for reclaimable memory, not durable data. Opt a specific actor out with a LongLivedStrategy.
The runtime reports on itself: metrics
Turn metrics on, queue a burst of messages that outruns one busy worker, and read collectMetrics() on a timer. The reporter is a plain function over the returned snapshot: no vendor SDK, no dependency the runtime pulled in. processingDuration adds the latency histogram, and the reporter turns it into an average and a couple of percentiles read straight off the buckets.
metrics/main.ts, make metrics
enqueuing 40000 messages, then watching the backlog drain
active=1 processed=2049 mailbox=37952 maxDepth=37952 deadletters=0 avg=0.278ms p50=0.25ms p95=0.5ms
active=1 processed=10241 mailbox=29760 maxDepth=29760 deadletters=0 avg=0.332ms p50=0.25ms p95=0.5ms
active=1 processed=24577 mailbox=15424 maxDepth=15424 deadletters=0 avg=0.186ms p50=0.05ms p95=0.5ms
active=1 processed=36865 mailbox=3136 maxDepth=3136 deadletters=0 avg=0.148ms p50=0.05ms p95=0.5ms
final fleet snapshot:
active=1 processed=40001 mailbox=0 maxDepth=0 deadletters=0 avg=0.142ms p50=0.05ms p95=0.5ms
worker: processed=40001 mailbox=0 restarts=0The mailbox jumps to the full backlog, then drains to zero as the worker catches up. Counts are exact; the latency numbers depend on the machine. The Metrics guide shows the snapshot in full and sketches an OpenTelemetry adapter you keep in your own code.
Across machines: remoting
A checkout node and a payments node as two Docker Compose services, each one actor system. The checkout desk resolves the payments actor with remoteLookup, charges through it with an ask piped back to its own mailbox, and watches it, so the payments actor stopping, or its whole node dying, arrives as the same Terminated message. The desk queues orders through the outage and flushes them when the node returns.
remoting/README.md, make remoting
checkout-1 | [checkout] order ord-0004: barista course (£899.00)
payments-1 | [payments] DECLINED ord-0004: over the charge limit
checkout-1 | [checkout] payments is GONE; queueing orders and re-resolving
checkout-1 | [checkout] ord-0016 queued; 1 order(s) waiting
checkout-1 | [checkout] payments connected: nodeakt://payments@172.19.0.2:5100/payments
checkout-1 | [checkout] flushing 3 queued order(s)This one needs Docker; the example's README also shows a two-terminal run with plain tsx. Kill the payments service mid-run and bring it back: the failover is the demo.
The capstone, a cluster: dns-actors
Three nodes discover each other over DNS and form one cluster, each a full actor system, driven by a small HTTP API. A worker spawned on one node is reached by name from every other; spawnOn spreads workers across the cluster by strategy; a singleton answers from the same instance everywhere. Then the node hosting a worker is killed with SIGKILL, no graceful goodbye, and the survivors detect the failure and recreate the worker with its configuration intact.
make actors boots the cluster and asserts every scenario:
dns-actors/README.md, make actors
booting the cluster...
PASS: three nodes formed the cluster
PASS: a distributed actor is reached by name from every node
PASS: spawnOn spread workers across all nodes
PASS: a singleton is reached by the same name from every node
hard-killing node1 (hosts orders)...
PASS: a crashed node's actor was recreated on a survivor with its configuration intact
all use cases passedThe last two lines are the point: a machine died mid-run and the actor it hosted came back on another, still answering to the same name. The example's README also walks the API by hand with curl, one scenario at a time, and docker compose stop shows the graceful leave taking the same recovery path. The Clustering section documents everything this example exercises.