Skip to content

Location-transparent messaging ​

In a cluster you message an actor by name, and the runtime routes to whichever node owns it. The PID you get back is a routed handle: tell, ask, request, watch, unWatch, forward, and pipeTo on it keep their call sites and their semantics, and the address decides whether the message stays local or crosses the wire. Replying to ctx.sender routes back to the sender's node, wherever that is.

An actor's path already carries the address of the node it lives on (nodeakt://orders@10.0.0.5:4000/name), and the distributed registry maps each name to its owning node. Looking a name up joins the two: read the owner from the registry, get a handle that routes there.

actorOf and actorOfAsync ​

There are two lookups, and the difference is whether they wait for the network.

ts
const local = system.actorOf("orders");            // PID | undefined, never blocks
const anywhere = await system.actorOfAsync("orders"); // Promise<PID | undefined>, resolves cross-node
actorOf(name)actorOfAsync(name)
ReturnsPID | undefinedPromise<PID | undefined>
Blocks on the networkNeverOnly on a cold miss (one registry read)
Local actorReturns itReturns it
Cross-node actor already in this node's viewReturns a routed handleReturns a routed handle
Cross-node actor not yet in this node's viewReturns undefinedReads the registry, returns a routed handle
Use it forThe hot path, local lookups, code that must not awaitReaching a name placed anywhere, the usual application lookup

actorOf answers instantly from a warm view, the routing cache this node builds from the placements it makes, from resolution reads, and from relocation events. That makes it perfect for the hot path, but it means the first lookup of a name owned by another node, one this node has not learned yet, reads as undefined even though a live owner exists. actorOfAsync closes that gap by awaiting a single registry read when the view is cold, so an application that reaches an actor placed on some other node gets it on the first call.

look up namelocal actor?routed handleyesnoin the warm view?yesnowhich lookup?undefinedactorOfread the registryactorOfAsyncfoundabsent

Rule of thumb: reach for actorOfAsync in application code that looks a name up to message it. Keep actorOf for lookups that must stay synchronous and are local or already warm.

What a routed handle does ​

A routed handle carries the remoting contract:

  • tell returns null when the transport accepted the message, not when the far mailbox did. An undeliverable message, an unknown name, a full mailbox, a stopped target, becomes a dead letter on the node that discovered it.
  • ask / request carry the reply back and settle on it; a timeout rejects with ErrRequestTimeout, sentinel errors keep their identity across the wire.
  • Ordering holds per actor: messages to one actor ride a single lane keyed by its path, so per-actor order survives the hop.
  • watch delivers one Terminated when the actor stops or its node dies; node death and connection loss are deliberately indistinguishable.
  • isRunning() is false for a routed handle; liveness across nodes is not synchronously knowable. Watch it instead.

Following a moved actor ​

A routed handle pins nothing. When an actor relocates to another node, the routing cache heals by re-resolving: the next time the resolver reads a cached owner that membership no longer lists, it drops the entry and reads the registry again, so lookups reach the actor's new home. A send that raced the move can be lost, an ask to the old owner is refused and re-resolves, a tell dead-letters, which is the message-loss window relocation accepts.

Watch is per-incarnation. A relocation stops the old instance and starts a fresh one, so a watcher receives Terminated even though the name lives on elsewhere. To keep following a relocatable name, re-look it up with actorOfAsync and re-watch; a RelocationCompleted subscriber can automate exactly that.

Released under the MIT License.