Skip to content

Receptionist

The Receptionist is a cluster-wide service registry. Each node hosts one Receptionist actor at a well-known path; registrations are local-authoritative (you trust your own node’s registrations); peers learn about foreign registrations through gossip.

import { ReceptionistId, ServiceKey } from 'actor-ts';
const receptionist = system.extension(ReceptionistId).start(cluster);
// 1. Define a typed key for this service
const apiKey = ServiceKey.of<ApiMsg>('api-service');
// 2. Register an actor under the key
const api = system.actorOf(Props.create(() => new ApiActor()), 'api');
receptionist.register(apiKey, api);
// 3. From any node — find every registered actor
const refs = await receptionist.find(apiKey);
console.log(`${refs.length} api actors in the cluster`);
// 4. Subscribe to changes
receptionist.subscribe(apiKey, (listing) => {
console.log(`current listing: ${listing.refs.length} actors`);
});

The receptionist is per-cluster — every node sees the same listing (with gossip lag, see below).

const key = ServiceKey.of<ApiMsg>('api-service');
// ^^^^^^^
// typed payload — consumers know what to tell

A ServiceKey<T> carries:

  • A string identifier — the human-readable key name.
  • A type parameter — the message type the registered actor accepts.

Type-safe lookup: await receptionist.find(apiKey) returns ActorRef<ApiMsg>[].

Keys are values — define them once, import them everywhere. Convention: in a shared Keys.ts module per app.

receptionist.register(key, actorRef);

Tells the local receptionist: “this actor on this node provides the service.” The receptionist:

  1. Adds the ref to its local map under key.
  2. Watches the ref so it auto-deregisters on stop.
  3. Gossips the addition to peers (next gossip round).
receptionist.deregister(key, actorRef);

Voluntary removal. Useful when an actor “leaves” a service without stopping (transient state change).

If the actor just stops normally, the receptionist sees Terminated and deregisters automatically — no manual cleanup needed.

const refs = await receptionist.find(key);
// ^^^^^^^^
// Array of every actor registered under this key, across the cluster

The find:

  1. Reads local registrations under key.
  2. Adds known remote registrations from gossip.
  3. Returns the combined list.

Returns immediately with the current local view — no synchronous cluster query. Means: registrations on other nodes that haven’t gossiped yet are missing.

Within a gossip round or two (1-2 seconds default), every node converges on the same view.

const unsubscribe = receptionist.subscribe(key, (listing) => {
console.log(`updated listing for ${key.id}: ${listing.refs.length} refs`);
});
// Later: unsubscribe();

Handler fires whenever the listing for key changes — locally (register/deregister/stop) or via incoming gossip.

Use for dynamic routing: an actor that subscribes to a key and updates its routing decisions when refs appear / disappear.

// In a cluster:
const receptionist = system.extension(ReceptionistId).start(cluster);
// Without a cluster (single node):
const receptionist = system.extension(ReceptionistId).start(null);

Without a cluster, the receptionist works locally only — useful for tests or single-node apps that still want the ServiceKey-based lookup API.

For clustered setups, always pass the cluster — otherwise remote registrations are invisible.

When MemberRemoved fires for a peer:

  • The receptionist forgets every registration that node contributed.
  • Subscribers fire with the updated (smaller) listing.

This handles the “node crashed, didn’t get a chance to deregister” case — gossip + cluster membership do the cleanup.

receptionist.register(apiKey, instance1);
receptionist.register(apiKey, instance2);
receptionist.register(apiKey, instance3); // all three under the same key
await receptionist.find(apiKey); // → [instance1, instance2, instance3]

A common pattern: N workers all registering under the same key. Consumers see the whole pool and can route however they like — round-robin, broadcast, random pick.

// One singleton registered for the cluster
receptionist.register(coordinatorKey, theCoordinator);
await receptionist.find(coordinatorKey); // → [theCoordinator]

For singleton-style services, the key has one ref. Consumers pick refs[0] (with a fallback for the empty case).

The receptionist doesn’t enforce single-instance — that’s the singleton manager’s job. But registering a singleton under a key gives consumers a discovery path that survives leadership changes.

NeedTool
Fixed routees per node, every node has themClusterRouter with a well-known path
Dynamic registrations, lookup by service nameReceptionist
Exactly one actor cluster-wideClusterSingleton (registered in receptionist for lookup if desired)
Per-key actors with auto-spawnClusterSharding

The receptionist is the most flexible discovery — but pay gossip cost for the dynamism. For static routing, the cluster router is cheaper.

The Receptionist API reference covers the full surface.