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 serviceconst apiKey = ServiceKey.of<ApiMsg>('api-service');
// 2. Register an actor under the keyconst api = system.actorOf(Props.create(() => new ApiActor()), 'api');receptionist.register(apiKey, api);
// 3. From any node — find every registered actorconst refs = await receptionist.find(apiKey);console.log(`${refs.length} api actors in the cluster`);
// 4. Subscribe to changesreceptionist.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).
Service keys
Section titled “Service keys”const key = ServiceKey.of<ApiMsg>('api-service');// ^^^^^^^// typed payload — consumers know what to tellA 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.
Registering
Section titled “Registering”receptionist.register(key, actorRef);Tells the local receptionist: “this actor on this node provides the service.” The receptionist:
- Adds the ref to its local map under
key. - Watches the ref so it auto-deregisters on stop.
- 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.
Finding
Section titled “Finding”const refs = await receptionist.find(key);// ^^^^^^^^// Array of every actor registered under this key, across the clusterThe find:
- Reads local registrations under
key. - Adds known remote registrations from gossip.
- 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.
Subscribing
Section titled “Subscribing”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.
Cluster-aware vs single-node
Section titled “Cluster-aware vs single-node”// 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.
Auto-cleanup on node leave
Section titled “Auto-cleanup on node leave”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.
Multiple actors per key
Section titled “Multiple actors per key”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.
Single actor per key (convention)
Section titled “Single actor per key (convention)”// One singleton registered for the clusterreceptionist.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.
When to use it vs alternatives
Section titled “When to use it vs alternatives”| Need | Tool |
|---|---|
| Fixed routees per node, every node has them | ClusterRouter with a well-known path |
| Dynamic registrations, lookup by service name | Receptionist |
| Exactly one actor cluster-wide | ClusterSingleton (registered in receptionist for lookup if desired) |
| Per-key actors with auto-spawn | ClusterSharding |
The receptionist is the most flexible discovery — but pay gossip cost for the dynamism. For static routing, the cluster router is cheaper.
Where to next
Section titled “Where to next”- Discovery overview — the bigger picture.
- Cluster overview — the membership underneath.
- Cluster router — static-path alternative for “well-known service” routing.
- Singleton overview — often registered in the receptionist for discovery.
The Receptionist API
reference covers the full surface.