defineActor({ type: 'Room', state: () => ({ members: [] as string[] }), async onActivate(ctx) { // runs after state is loaded and migrated, before the first turn ctx.index = new Map(ctx.state.members.map((m) => [m, true])); }, async onDeactivate(ctx, reason) { // reason: 'idle' | 'shutdown' | 'capacity' | 'migrated' | … },});
onActivate always observes migrated state. A throw
from it fails activation with ActorActivationError and the call that triggered it rejects.
A failed activation fails every parked caller and is forgotten, so the next caller activates
it again — a loop throttled only by its callers. Dev builds say so once per streak, when the
same actor activates-and-fails three times within 30 seconds:
[sigx actors] Cart/user-42 activated-and-failed 3 times in 812ms. …
A throwing onActivate, state load or migrateState all count; a placement refusal does not,
and a successful activation resets the streak. Fix the activation, or back off at the call
site.
A write-behind actor has one more hook, onStateError(ctx, error, phase), for a save that
has no caller to throw to — see
State & persistence.
another activation won an etag conflict; this one's unsaved writes are discarded and the next call loads the winner. After a failed write-behind flush it fires right after onStateError, with no further call. With retryQueuedOnConflict, a turn-path conflict never reaches it
idleAfterMs (default 20 minutes) is how long an activation survives without a call. The
sweeper runs every sweepIntervalMs (default 60s); set either to 0 to disable.
Two things count as activity that people expect not to:
An open live watch keeps the actor alive, deliberately — so idle
collection cannot deactivate an actor out from under a subscriber.
A running task holds a keep-alive until it finishes.
A ctx.timer tick does not keep the actor alive, and that is intentional: a volatile
timer is a convenience, not a reason to pin an object in memory. If you need the actor to wake
up later regardless, use a durable reminder.
A soft LRU cap, enforced by the sweeper, with deactivation reason capacity. Soft is the
operative word:
Busy actors and actors holding a keep-alive are never shed.
The live count may therefore exceed the cap.
State survives — a shed actor reactivates from storage on its next call.
It is a memory guardrail, not an admission-control mechanism — for that, see
Deadlines & admission. Watch byReason.capacity in
metrics: a cap that is shedding constantly is either too low or
hiding a key-cardinality problem.
Returns { type, key, queued, ageMs, idleMs, keptAlive, tasks } per activation. sortBy
accepts 'queued', 'age' or 'idle', and type filters to one actor type.
queued is the one to look at first: a persistently non-zero queue depth is a hotspot, and
the fix is usually to shard the key or move I/O out of the turn — see
Metrics for the queueMs versus turnMs distinction.
Actor keys can be personal data. host.activations() returns them, which is why the ops
endpoint keeps the activation list off by default and requires a secret.
Stops accepting new calls, drains in-flight turns, flushes pending write-behind saves, runs
onDeactivate for every activation and then the app's onStop hooks in reverse order.
Detached task runs get taskGraceMs (default 10s) to wind down and
finish their bookkeeping — including a run that completed just as the stop landed — and that
grace counts against timeoutMs.
On Node, wire it to signals with attachSignalHandlers — see
Running a host on Node. In a cluster, a graceful stop announces leaving
before handing off, which is why liveness and readiness
are allowed to disagree.