Almost every system ends up with somewhere to put the work that cannot happen right now: a queue, a worker, a retry table, and a sentence in the README explaining that things are eventually consistent. This demo takes the other route — the developer draws a line through the work an event implies, says what happens before the answer and what happens after, and the deferred side is measured rather than hoped for.
Did anyone choose eventual consistency, or did it arrive with the machinery that was installed to defer the work?
Formally, everything an operation implies belongs before the answer: if the rules say it follows, it should be settled before the caller hears back. The split into now and later is not derived from that — it is a practical cut the developer makes, feature by feature, against a latency budget and against what is worth doing in parallel. Drawing the cut and deciding where the deferred half runs are one act, not two.
Which is why the paper's own summary of the consequence reads the way it does: eventual consistency is the shadow of the partition, not the design choice.
It is not only a way to answer faster. It is also where the operational world is kept — and being kept there is what stops it spreading.
- Deferral, for the work an operation cannot afford inside its latency budget or that is better run in parallel.
- Segregation, for the extrinsic concerns — email, reporting, telemetry, webhooks — which land in the deferred artifact instead of in the rules. Not their absence: their containment. The artifact calls the technology, and no other operation inherits the dependency.
- The containment is enforced by the language rather than by convention: the deferred branch's outbound path runs as a query, so it cannot write back into the actor's state. An operational concern that tries to leak back does not compile into the journal — it is refused.
The published laboratory was re-run for this page on ordinary hardware — a different machine from the paper's — and the two kinds of result behaved differently, which is the useful part.
| Published | Re-run here | |
|---|---|---|
| Deferred effects fired, of 2,000 | 2,000 | 2,000 |
| Missed | 0 | 0 |
| Fired twice | 0 | 0 |
| Median time from operation to deferred effect | ≈1.3 ms | 0.86 ms |
| 99th percentile | ≈2 ms | 2.66 ms |
| Operation dispatch and persist | ≈0.40 µs | 0.91 µs |
The properties reproduced exactly and the timings did not, which is what should happen: a slower machine moves a millisecond, and moves nothing about whether an effect fired once. A separate run put fifty actors' matches into the journal before the deferred branch was even listening, then restarted it — nothing was lost and nothing fired twice.
The whole arrangement rests on one unglamorous condition: deferring has to be cheaper than not deferring.
If declaring deferred work costs a developer more lines or more thought than writing it inline, nobody declares it. Then two things go wrong at once — operations get slower, and the rules get contaminated with the operational world, which is the worse of the two because it is hard to undo and it spreads. The paper puts that squarely on the framework rather than on developer discipline: low friction is an architectural responsibility, not a virtue to be asked for.
The paper names three situations where its own construct does not apply, and they are worth reading before the ones where it does.
- Where every effect must commit in lockstep with the answer — core ledgers, some regulated systems. The line is still drawn; it is drawn at zero, with nothing deferred. What is forbidden is deferring an effect whose delivery is at-least-once.
- Where a team does not want to make the decision. Treating the deferred branch as fire-and-forget loses exactly the property it was for. The framework can make the decision cheap; it cannot make it for anyone.
- Where the deferred half failing has to undo the answer already given. Some long-running workflows are like that, and for those the paper says plainly that this is not the right primitive.