Problem
Both public ways to stop another actor queue behind its backlog:
ref.stop() / system.stop(ref) / context.stop(ref) send PoisonPill as an ordinary user message (src/ActorRef.ts:97, src/internal/ActorCell.ts:527-529) — stop-after-drain, by design.
ref.kill() enqueues Kill the same way (src/ActorRef.ts:100), so even the "hard" variant waits until everything already queued has been processed before it raises ActorKilledError.
The immediate path exists: the terminate system command jumps the queue, because ActorCell.run() drains system messages before each user message (src/internal/ActorCell.ts:801-811) and finalizeTermination dead-letters the remainder (src/internal/ActorCell.ts:984-986). But it is reachable only for the actor itself (context.stopSelf(), src/internal/ActorCell.ts:531-533) or via the hierarchy (parent teardown / system.terminate()).
So an overloaded or runaway actor with, say, 10k queued messages cannot be taken down from outside without processing all 10k first — or tearing down its parent.
The framework itself hits this gap: the cluster-singleton handover stops the old instance with a PoisonPill queued behind that child's whole mailbox — one half of #949.
Proposed API
// ActorRef — sibling of stop() and kill()
/**
* Stop this actor immediately. The currently running handler finishes
* (processing is cooperative, never preemptive), the rest of the user
* queue goes to dead letters, children stop first, watchers receive
* Terminated — the same path context.stopSelf() takes today.
*/
stopNow(): void;
LocalActorRef.stopNow() → getCell().enqueueSystem({ kind: 'terminate' }) — the existing TerminateCommand, no new mechanism.
- Pass-throughs where
stop already exists: ActorSystem.stopNow(ref) (src/ActorSystem.ts:410-412), ActorContext.stopNow(ref) (src/ActorContext.ts:122-126).
- Remote refs: out of scope for the first cut. A cross-node immediate stop needs a wire-level system command; until that exists,
RemoteActorRef.stopNow() should fail loudly (throw) rather than silently degrade to drain semantics.
Alternatives considered
ref.kill() — still a user message; with a deep backlog it acts as late as stop().
- Stopping the parent — works via the system-command cascade, but takes down siblings and is the wrong altitude for "remove this one actor".
- Reaching into
LocalActorRef.getCell().enqueueSystem(...) from user code — internal API, not a supported surface.
Acceptance criteria
- An actor with N pending user messages terminates after at most the one in-flight message once
stopNow() is called; the N pending messages arrive in dead letters.
- Children stop before the parent finalizes; watchers receive
Terminated — identical to stopSelf().
stop() / kill() semantics unchanged (drain).
- Lifecycle docs (EN + DE) document the three flavours side by side: drain (
stop), supervised kill (kill), immediate (stopNow).
Related: #663 (draining shutdown — the complementary direction), #949 (singleton handover queues PoisonPill behind a full mailbox).
Problem
Both public ways to stop another actor queue behind its backlog:
ref.stop()/system.stop(ref)/context.stop(ref)sendPoisonPillas an ordinary user message (src/ActorRef.ts:97,src/internal/ActorCell.ts:527-529) — stop-after-drain, by design.ref.kill()enqueuesKillthe same way (src/ActorRef.ts:100), so even the "hard" variant waits until everything already queued has been processed before it raisesActorKilledError.The immediate path exists: the
terminatesystem command jumps the queue, becauseActorCell.run()drains system messages before each user message (src/internal/ActorCell.ts:801-811) andfinalizeTerminationdead-letters the remainder (src/internal/ActorCell.ts:984-986). But it is reachable only for the actor itself (context.stopSelf(),src/internal/ActorCell.ts:531-533) or via the hierarchy (parent teardown /system.terminate()).So an overloaded or runaway actor with, say, 10k queued messages cannot be taken down from outside without processing all 10k first — or tearing down its parent.
The framework itself hits this gap: the cluster-singleton handover stops the old instance with a
PoisonPillqueued behind that child's whole mailbox — one half of #949.Proposed API
LocalActorRef.stopNow()→getCell().enqueueSystem({ kind: 'terminate' })— the existingTerminateCommand, no new mechanism.stopalready exists:ActorSystem.stopNow(ref)(src/ActorSystem.ts:410-412),ActorContext.stopNow(ref)(src/ActorContext.ts:122-126).RemoteActorRef.stopNow()should fail loudly (throw) rather than silently degrade to drain semantics.Alternatives considered
ref.kill()— still a user message; with a deep backlog it acts as late asstop().LocalActorRef.getCell().enqueueSystem(...)from user code — internal API, not a supported surface.Acceptance criteria
stopNow()is called; the N pending messages arrive in dead letters.Terminated— identical tostopSelf().stop()/kill()semantics unchanged (drain).stop), supervised kill (kill), immediate (stopNow).Related: #663 (draining shutdown — the complementary direction), #949 (singleton handover queues
PoisonPillbehind a full mailbox).