Actor state is race-free because of one mechanism, and it is worth understanding
before anything else here: an activation runs its work in turns, and by default
only one turn runs at a time.
A turn is one execution of one method against one activation. It begins when the runtime
dispatches the call and ends when the method's promise settles — not when the method stops
running.
That distinction is the whole thing:
TypeScript
async checkout() { const quote = await fetch('https://slow.example/quote'); // the turn is still open ctx.state.total = await quote.json(); await ctx.save();} // now the turn ends
Between two awaits nothing else touches this actor's state. That is why
ctx.state.items.push(item) is safe with no lock, no transaction and no compare-and-set loop.
Each new turn attaches to the tail of a promise chain with .then(). Nothing is stored between
them, so the ordering you get is the ordering of arrival and nothing else.
That has a practical consequence worth knowing before you go looking: the queued number in
host.activations() and the
ops endpoint is a counter, not the length of a structure you
can inspect. There is no priority lane to configure, no bounded-queue policy to tune, and no
way to drop or reorder pending work.
If your design needs any of those, put them in your own code in front of the actor — a queue
you own, with the policy you want, calling the actor as its consumer.
An activation runs turns in one of two ways, and which one a call takes is a property of the
actor or the method, not of the caller.
The serial lane is the default. Each turn waits for the previous one to settle. This is
what makes plain mutation safe.
The interleaved lane does not wait and is not waited for. A call takes it when the actor
declares reentrant: 'always', or when the method is named in
methodReentrancy.
sequenceDiagram
participant A as Caller A
participant B as Caller B
participant R as Reader
participant C as Cart/user-42
A->>C: checkout
activate C
Note over C: serial turn 1
B->>C: addItem
Note right of B: waits for turn 1
R->>C: stats
Note right of R: interleaved, runs now
C-->>R: result
C-->>A: done
deactivate C
activate C
Note over C: serial turn 2
C-->>B: done
deactivate C
The serial lane blocks; the interleaved lane does not
The consequence is the trade you are making when you reach for the interleaved lane: a turn
there can observe state changing across every await, so it must re-read rather than cache
anything another turn may have moved.
Because a turn ends when the promise settles, an awaited call inside a method keeps the serial
lane occupied for its whole duration:
TypeScript
async checkout() { await fetch('https://slow.example/quote'); // every queued turn for THIS actor waits}
Nothing else is affected — other actors are other activations — but this one is blocked. Dev
builds warn when a turn exceeds slowTurnMs (default 5s), and a persistently non-zero queued
count is the same symptom seen from outside.
This is a deliberate trade: state safety over concurrency within one actor. The runtime
could have released the lane at each await and given you neither.
When it hurts, the three ways out, in order of preference:
Move the I/O out of the turn — do the slow call in the caller and pass the result in.
Move it to a task, which runs outside the turn sequence entirely
and re-enters through ctx.turn() only to touch state.
Let that one method interleave with
methodReentrancy — the usual fit for a read that must not queue
behind slow writes.
The runtime has two things called a scheduler, and conflating them will confuse a debugging
session:
Decides
Turn scheduling
in what order and with what overlap an activation's turns run. No clock involved.
ActorScheduler
when in wall-clock time something fires — timers, reminder ticks, idle sweeps, write-behind flushes. Swappable via CreateHostOptions.scheduler; manualScheduler() makes time deterministic in tests.
If your question is "why did these two calls overlap?", it is the first. If it is "why did this
fire late?", it is the second — see
Timers & reminders and the coarse-deadline note in
Errors.