- Delivery is ordered per actor identity and at least once.
- Different identities may execute concurrently.
- Sequence allocation and durable enqueue are one transaction.
- An actor reads its own schedule. A read applies the intents staged so far in the turn, so it agrees with what the commit will write rather than with what the turn began with.
- A reminder can be cancelled. A cancellation commits with the state change that decided it, and applies in the order the turn called it. A cancellation cannot recall an occurrence the scheduler already turned into a message. It does pre-empt one the scheduler claimed but has not yet enqueued, and the scheduler treats that as ordinary work rather than a failure.
- Concurrent callers that create the same actor produce one instance row and distinct sequences. The mailbox locks that row by its primary key, so MySQL does not upgrade a shared lock and the enqueue does not deadlock.
- A retryable failure rolls state and staged intents back and blocks later work.
- A stale activation may finish JavaScript but cannot commit.
- Effects can execute more than once and must deduplicate by their stable id.
- Destruction creates an incarnation boundary; old leases cannot address a
recreated actor. A caller authorized before destruction receives
ActorDestroyed; an unknown or forged reference remains unauthorized. - Graceful process shutdown and stale-process cleanup use the same atomic ownership release. Claimed messages return to ready membership. The runtime unfences the activations. Effect, reminder, and broadcast claims become available again. A stale process that drains is recoverable like a stale process that runs.
- Permanent operation failure raises
MessageFailed. The error carries the durable message ID and the persisted error details. Actor code text is not the public exception contract. - Operation, lifecycle, observable, and payload callbacks retain their owning runtime through async context. Isolated runtimes therefore never fall back to a global default while actor-owned code resolves another actor reference.
- Observable outbox rows are claimed in actor revision order. Subscription sessions and browser clients reject duplicate or stale revisions within an incarnation, while a recreated actor starts a new revision sequence.
- Wake-up notifications happen only after commit and never replace polling,
except with
singleOwner. Workers watch before claiming, so an in-process notification between an empty claim and the following wait is retained. - Activation passes are bounded. Yielding changes ready-membership polling order only; it neither changes durable message sequence nor makes future work due early.
- Idle hydrated actors keep the same renewable lease as their fence. Cache reuse never bypasses claim membership or the commit fence. A failed turn restores its public fields before reuse. Conditional release cannot clear a newer owner or generation.
- Actor setup completes before an attempt begins. A hydration, migration, or activation failure restores ready membership, releases its activation fence, and restores the attempt count. All three happen in one atomic step. A caller that waits receives the setup error.
guardApplicationDatabase()rejects direct application writes during actor operations, observable and payload projections, and state migrations. It permits onlySELECTthrough row-returning methods. Commit actions remain in that read-only context and may write only through their supplied fenced connection. The guarantee applies only to clients passed through the facade.- Snapshots hydrate one committed state image and evaluate every inferred getter against it. Getter mutation or staged durable work rejects the whole snapshot; successful snapshots and their nested JSON values are frozen copies.
- Personalized payloads hydrate committed state separately for every payload name and subscriber. Each projection is read-only, size bounded, and fenced independently by actor incarnation and revision. One projection that is denied, that mutates, or that fails cannot stop its siblings or observable delivery. A state change on an actor with payloads creates a revision broadcast, even when that actor declares no scalar observables.
With singleOwner: true, an idle runtime sends no query, and it still runs all
work when the work arrives or comes due. This guarantee holds only while one
process writes the database.
The guarantee rests on these rules:
- Every write that adds work or moves it earlier happens in this process. After
the commit, the write notifies an in-process signal for each role that must
react. A host that enqueues with
runtime.enqueueInternalMessageInTransaction()must callruntime.announceInternalMessage()after its commit, as it must without the option. Without that call, the message waits for the next wake-up. - A role watches the signal before it claims, so it cannot lose a notification that arrives during a pass.
- After an empty pass, a role reads the earliest time at which a claim can take something. The read uses the same conditions as the claims: the head of each mailbox, lease ends, pending effects, broadcasts, and reminders, claims whose owners become stale, and recovery timeouts. The role waits until that time, or for at most 24 hours.
- A claim writes the row of the claiming worker in the claim transaction. A
worker that was idle for longer than
processAliveThresholdMillisecondstherefore never holds work under a stale row, and the alive rule protects running work as it does without the option.
Fencing and at-least-once delivery do not change. Claims, activation leases, the commit fence, retries, dead letters, and effect recovery use the same rows and the same conditions.
The guarantee has these limits:
- One process only. A write from another process, for example a second runtime, a script, or a command, does not wake this runtime. That work waits for the next local wake-up or due time, for at most 24 hours.
- The start check reads the process rows once. It sees another writer only while that writer's row is fresh, and an idle single owner does not refresh its row. Two processes that start at the same moment can both pass the check. Only an operating system lock, such as SQLite's exclusive locking mode, keeps a second process out.
- A due time runs on a Node timer. A long synchronous handler delays the timer, as it delays polling. Node timers stop while the computer sleeps. A watch sees the jump of the wall clock within 10 seconds and makes every role read its due time again, so work that came due during a sleep runs at most about 10 seconds after the computer wakes.
- When a due time is now or has passed and a claim still finds nothing, for example after an actor setup failure, the role retries after 1 ms and then doubles the wait up to its polling interval. Stuck work is then retried at the polling interval, as it is without the option.
- At-least-once execution means actor code may begin more than once. State and
staged intents from a failed turn roll back, but arbitrary external work does
not. External systems need stable idempotency keys. A test shows this
clause:
pnpm run test:at-least-oncecrashes an effect worker between the sink write and the acknowledgement, restarts it, and shows the sink reading 2 with deduplication off. It then shows a guard on the stable effect id absorbing the same duplicate, with the sink reading 1. The state commit happens exactly once in both runs. - The activation fence protects the Solid Objects commit. It cannot revoke or undo network calls, files, emails, payments, or other external effects.
- One identity processes one write operation at a time. This is the ordering guarantee and also the hot-identity throughput limit.
- A commit is scoped to one actor turn. There is no transaction spanning two actor identities.
- Processes with incompatible
stateVersiondefinitions cannot safely overlap. Once newer code persists a state version, older code rejects that actor. - The application owns HTTP, WebSocket authentication, rendering, process placement, capacity, database backups, and database failover.
- Redis and PostgreSQL notifications reduce wake-up latency. They do not replace durable polling, and they hold no authoritative state.
- The browser platform has no
AsyncLocalStorage. Its context store covers only the synchronous part of a callback. After the firstawaitinside an actor operation, the ambient guards (applicationWritesForbidden()and the inside-transaction check) read as unset. Durable-state fencing, mailbox ordering, and the SQLite WASM deadline enforcement do not depend on those guards. The guards are best-effort in the browser and exact in Node. snapshotWithIncarnationidentifies a persisted incarnation byinstanceId. Use exact ID equality, with numeric revision comparison only within that ID.createdAtMsis millisecond creation metadata; destruction and recreation can produce equal timestamps and revisions while their IDs differ. Neither a timestamp nor a random UUID provides a total incarnation order. Snapshot reads alone do not atomically fence an external write; see Snapshot incarnation identity.- Large documents, bulk pipelines, globally placed edge state, and global counters are outside the intended workload. Prefer an ordinary row transaction when it completely enforces the invariant.