Receive timeout
A receive timeout is the actor-bound equivalent of “this thing
has been quiet for too long — do something.” You set a duration;
if no user message arrives within that window, the framework
delivers a ReceiveTimeout system message to the actor.
import { Actor, ReceiveTimeout } from 'actor-ts';
type Msg = { kind: 'activity' } | ReceiveTimeout;
class Session extends Actor<Msg> { override preStart(): void { // Idle for 30 s → fire a ReceiveTimeout. this.context.setReceiveTimeout(30_000); }
override onReceive(msg: Msg): void { if (msg === ReceiveTimeout.instance) { this.log.info('session idle — stopping'); this.context.stopSelf(); return; } // Any user message resets the idle timer. this.log.info('activity'); }}Three things to notice:
setReceiveTimeout(ms)arms the timer. Aftermsof no user messages,ReceiveTimeout.instancelands in the mailbox.- Every user message resets the timer. The clock starts over
from each
onReceiveinvocation. Activity = the actor is alive; silence = the timer counts down. - The timeout fires repeatedly — once delivered, the timer
re-arms. An actor that doesn’t process the
ReceiveTimeoutkeeps receiving them everyms. UsecancelReceiveTimeout()orsetReceiveTimeout(0)to stop.
The API
Section titled “The API”interface ActorContext<TMsg> { setReceiveTimeout(ms: number): void; cancelReceiveTimeout(): void;}Two methods. setReceiveTimeout(0) is equivalent to
cancelReceiveTimeout() — the timer is disabled. Passing a
positive value (re-)arms it.
Common patterns
Section titled “Common patterns”Session expiry
Section titled “Session expiry”class Session extends Actor<SessionMsg | ReceiveTimeout> { constructor(private readonly userId: string) { super(); }
override preStart(): void { this.context.setReceiveTimeout(15 * 60_000); // 15 minutes }
override onReceive(msg: SessionMsg | ReceiveTimeout): void { if (msg === ReceiveTimeout.instance) { this.log.info(`session ${this.userId} expired`); this.context.stopSelf(); return; } // Process the session message... }}A per-user session actor that auto-stops after 15 minutes of inactivity. When the user comes back, a fresh actor spawns; the old one is gone. This is the bread-and-butter use of receive timeouts.
Passivation in sharded entities
Section titled “Passivation in sharded entities”class Entity extends Actor<EntityMsg | ReceiveTimeout> { override preStart(): void { this.context.setReceiveTimeout(2 * 60_000); // 2 minutes idle }
override onReceive(msg: EntityMsg | ReceiveTimeout): void { if (msg === ReceiveTimeout.instance) { // Tell the shard region we're done — it will stop us cleanly. this.context.parent.forEach((p) => p.tell({ kind: 'passivate', entityId: this.id })); return; } // ... }}Cluster sharding pairs naturally with receive timeouts: an entity that’s been idle for some time signals its region to passivate it, freeing memory. When a new message for the same entity-id arrives, the region re-spawns the entity from its persisted state.
See Sharding for the full passivation protocol.
Watchdog for downstream silence
Section titled “Watchdog for downstream silence”class Heartbeat extends Actor<...> { override preStart(): void { // We expect the upstream to push at least every 30 s. this.context.setReceiveTimeout(30_000); this.upstream.tell({ kind: 'subscribe', subscriber: this.self }); }
override onReceive(msg): void { if (msg === ReceiveTimeout.instance) { this.log.warn('upstream silent — re-subscribing'); this.upstream.tell({ kind: 'subscribe', subscriber: this.self }); // Don't stop — keep trying. return; } // Process the heartbeat... }}Catching “the upstream stopped sending events” without an explicit
ping from this side. The receive timeout is the absence of
traffic — it fires precisely when there’s nothing else to react to.
Interaction with system messages
Section titled “Interaction with system messages”The timer counts user messages only. System messages
(PoisonPill, Kill, supervisor signals, internal events) do
not reset it. This matters because:
- An actor with a long-running system-message stream (e.g.
receiving
Terminatedfrom many watched children) still triggers receive timeouts if no user messages arrive. - Likewise, a quiet actor that receives a
PoisonPillfrom outside doesn’t get a “last receive timeout” before stopping — the PoisonPill triggers normal shutdown, the receive-timeout state is cleaned up.
Interaction with become
Section titled “Interaction with become”Switching behavior with context.become(...) does not affect
the receive timeout. The same ms window applies to whichever
handler is currently active. Two phases of an actor share one
timeout — re-arm explicitly in become if you want phase-specific
durations:
private idle = (msg) => { if (msg.kind === 'start') { this.context.become(this.working); this.context.setReceiveTimeout(60_000); // 1 min idle in working mode }};
private working = (msg) => { if (msg === ReceiveTimeout.instance) { this.context.become(this.idle); this.context.setReceiveTimeout(0); // no timeout in idle mode }};ReceiveTimeout messages and the mailbox
Section titled “ReceiveTimeout messages and the mailbox”ReceiveTimeout.instance is delivered via the same path as any
other user message — it’s enqueued, dispatched, handed to
onReceive. Three knock-on effects:
- A backed-up mailbox delays receive timeouts. If the actor
has 100 user messages queued and processes them slowly, the
ReceiveTimeoutfrom a quiet period earlier might land after some of those messages — by which point the actor isn’t idle anymore. This is usually fine (a busy actor isn’t idle by definition), but the timing isn’t guaranteed. ReceiveTimeout.instanceis a singleton, so identity comparison (msg === ReceiveTimeout.instance) is reliable.- Restart resets the timer. On restart, the new instance
starts with no receive-timeout configured; call
setReceiveTimeoutinpreStart(orpostRestart) to re-arm.
Where to next
Section titled “Where to next”- Timers and scheduling — for scheduled fires; receive timeout is for idle-detection.
- Sharding — Remember entities — receive-timeout-driven passivation in cluster setups.
- Actor — the base class
whose
context.setReceiveTimeoutyou call. - Mailboxes — where ReceiveTimeout messages land.
The ActorContext.setReceiveTimeout
and ReceiveTimeout API
references cover the full signatures.