Dispatchers
A dispatcher schedules the execution of actor message-processing
units. Whenever an actor’s mailbox has a message ready to process,
the dispatcher decides when the runtime pulls it and runs
onReceive.
JavaScript is single-threaded, but it has two interleaving primitives
for “do this later”: the microtask queue (queueMicrotask,
Promise.then) and the macrotask queue (setImmediate,
setTimeout(0)). Picking between them is the dispatcher’s job, and
the choice has real consequences for throughput, fairness, and
latency.
The three built-in dispatchers
Section titled “The three built-in dispatchers”The framework ships three, exposed as classes you instantiate and
pass to ActorSystem.create:
| Dispatcher | Schedules via | Trade-off |
|---|---|---|
MicrotaskDispatcher | queueMicrotask | Fastest. Can starve I/O and timers under sustained actor load. |
ImmediateDispatcher (default) | setImmediate or setTimeout(0) | Lets I/O + timers interleave between actor messages. Slightly higher per-message latency. |
ThroughputDispatcher | setImmediate with a configurable run-N-then-yield budget | Like ImmediateDispatcher but processes up to throughput messages back-to-back before yielding. Balances throughput against fairness. |
The default is ImmediateDispatcher — fairness with I/O is the right
default for HTTP servers and broker-backed actors, both of which are
common in actor-ts applications.
Picking a dispatcher
Section titled “Picking a dispatcher”import { ActorSystem, MicrotaskDispatcher } from 'actor-ts';
// Use microtasks: maximum throughput, minimum scheduling overhead.// Pick when you have no I/O at all OR I/O is so rare that starvation// isn't a concern.const system = ActorSystem.create('compute-heavy', { dispatcher: new MicrotaskDispatcher(),});import { ActorSystem, ThroughputDispatcher } from 'actor-ts';
const system = ActorSystem.create('mixed-workload', { dispatcher: new ThroughputDispatcher(50), // run up to 50 messages per actor before yielding to I/O. Default // is ~5 in `ImmediateDispatcher`; bumping it to 50 wins on // throughput at the cost of HTTP-response latency.});When the dispatcher choice matters
Section titled “When the dispatcher choice matters”For most apps, the default is fine. Three situations where it isn’t:
- Compute-heavy actor pipelines with no I/O. An ETL-style job
that reads from a journal, transforms in actors, writes to another
journal — no live HTTP requests, no broker callbacks.
MicrotaskDispatcherhere removes ~50 % of per-message overhead vs. immediate. - Latency-sensitive HTTP servers. HTTP responses need to flush
promptly; if your actors monopolize the event loop, response
latency grows. Stay on
ImmediateDispatcher(the default) or drop to a smaller throughput budget. - Tests that need deterministic ordering. Microtasks complete
before the next macrotask, so
MicrotaskDispatcheris preferred for tests that expect “send N messages, observe all N effects” without a yield between them. See TestKit for the test-specific dispatcher.
Writing a custom dispatcher
Section titled “Writing a custom dispatcher”The Dispatcher interface is tiny:
interface Dispatcher { readonly id: string; execute(fn: () => void | Promise<void>): void;}Implement these two and you have a custom dispatcher. Common reasons to do that:
- Tracing: wrap the work-unit to attach an OpenTelemetry span so every actor message gets its own trace context.
- Metering: count messages processed, report to a metrics collector.
- Per-priority isolation: keep a “fast lane” for system actors
while user actors run on a separate queue. (For most apps,
per-actor priorities via
PriorityMailboxare enough; per- dispatcher isolation is an advanced case.)
import type { Dispatcher } from 'actor-ts';
class TracingDispatcher implements Dispatcher { readonly id = 'tracing-dispatcher'; constructor(private readonly inner: Dispatcher) {} execute(fn: () => void | Promise<void>): void { this.inner.execute(() => withTraceContext(fn)); }}Wrap-and-delegate is the usual shape — keep the underlying scheduling behavior, just add cross-cutting concerns.
Per-actor dispatcher
Section titled “Per-actor dispatcher”The actor system has one default dispatcher. Individual actors
can specify their own via Props:
import { Props, MicrotaskDispatcher } from 'actor-ts';
const fastActor = system.actorOf( Props.create(() => new Crunchy()) .withDispatcher(new MicrotaskDispatcher()),);Most apps don’t need per-actor dispatchers — the system-level one applies uniformly. Reach for it when you have a mixed workload where some actors need throughput while others need fairness with I/O on the same node.
Where to next
Section titled “Where to next”- Mailboxes — the queue the dispatcher pulls from. FIFO / bounded / priority.
- Timers and scheduling — actor-bound timers; uses the scheduler, not the dispatcher.
- ActorSystem — passing a dispatcher via the settings argument.
- TestKit — the test-specific dispatcher that runs synchronously for assertion-friendly tests.