Skip to content

Placement ​

In a cluster a top-level actor name is unique across every node. Where the actor runs is placement, and there are two ways to decide it:

  • spawn(name, props) keeps the actor on the calling node, as always. The name is now claimed cluster-wide.
  • spawnOn(name, props, { strategy }) places the actor on the node a strategy chooses, and returns a routed handle to it.

Either way, a name → owning node record goes into the distributed registry, and any node can then reach the actor by name.

Cluster-wide unique names ​

spawn("orders") on two nodes at once is a race for the one name. The winner builds the actor; the loser is refused with ErrActorAlreadyExists, exactly as a duplicate spawn is refused on a single node. Handing the loser a handle to an actor built from someone else's Props would turn a race into silent misconfiguration, so the loser is told, not quietly aliased. A caller that wants the existing actor looks it up with actorOf; a caller that wants idempotent creation uses a singleton.

ts
const ref = await system.spawn("orders", Props.create(Orders, "eu"));
// On another node, at the same instant:
await system.spawn("orders", Props.create(Orders, "us")); // rejects: ErrActorAlreadyExists

Placing on a chosen node ​

spawnOn places an actor on a node the caller does not run on, choosing the node by a strategy carried in the options, not a raw address, so call sites express intent, not topology.

ts
const ref = await system.spawnOn("worker-42", Props.create(Worker, config), { strategy: "leastLoad" });

The options are the SpawnOnOptions type, whose strategy is a PlacementStrategy. On a system created without clustering, spawnOn and spawnSingleton reject with ErrClusteringDisabled.

The owner is the one that claims the name and builds the actor, so a spawnOn that loses the name race is refused with ErrActorAlreadyExists just like a spawn. The returned PID is a routed handle: tell, ask, request, watch, forward, and pipeTo on it deliver to whichever node owns the actor.

CallerOwnerRegistryship the recipeclaim the namename freeclaimedbuild the actorrouted handlename heldrefusedErrActorAlreadyExists

The owner does the single claim; the caller never pre-claims. That is what keeps two callers racing the same name from both writing a record. When the strategy picks the calling node itself, the same claim-and-build happens locally with no network hop.

Strategies ​

The strategies are a closed set of four. A raw address is deliberately not a strategy.

StrategyChoosesNotes
roundRobinThe next member in rotationThe default. Rides an atomic cluster counter, so every node advances the same rotation and load spreads with no coordinator on the path.
randomA uniformly random live member
localThe calling nodeOpt a call site out of distribution without changing its shape.
leastLoadThe member owning the fewest actorsTallies every host's load in one registry scan, so choosing costs one scan whatever the cluster's size.

Every strategy considers only live, ready, non-draining members, so a node still joining or already leaving is never chosen.

What crosses to the owner ​

spawnOn builds the actor on the owning node, so the construction crosses the wire as a recipe, exactly as a remote spawn does:

  • The actor class must be registered under the same name on both nodes (registerActor(Worker) at module scope). An unregistered class rejects with ActorNotRegisteredError.
  • Constructor arguments cross the wire codec, validated structured-cloneable. The reentrancy, passivationStrategy, and relocatable options travel; live-object options (mailbox, supervisor) are node-local and refused, as in every Props spawn.

Because the recipe is what a survivor also needs to rebuild the actor after a crash, a placed actor stores it for relocation automatically.

Opting an actor out of relocation ​

By default a placed actor is recreated on a survivor when its node departs. Set relocatable: false to bind it to its node instead, so it dies with the node rather than moving:

ts
await system.spawnOn("gpu-worker", Props.create(GpuWorker), { strategy: "leastLoad", relocatable: false });

The system-wide default is the cluster.relocation option; the per-actor relocatable wins over it. See relocation.

Released under the MIT License.