Replay determinism
A Socratic walk-through of replay determinism — reasoned out one step at a time, not lectured.
The question we started with
THE QUESTION #Why must code that will be re-run from the beginning be forbidden from asking what time it is?
Reading the clock is about the most innocent thing a program can do. It touches nothing, changes nothing, and cannot fail in any interesting way. Yet a workflow engine will refuse to let your code do it, and if you smuggle it in anyway the engine may halt the whole execution and declare it broken.
What could possibly be dangerous about a function that only reads? The rule looks like superstition until you ask a different question: not what does this call do to the world, but what does it do to a second run of the same code?
Reasoning it through
REASONING #Recall the situation this code lives in. Its progress is preserved as an ordered record of results, and recovery works by running the function again from line one while answering every already-completed step from that record. So the same source code executes at least twice, and the record is matched to it by position in the sequence of operations, not by anything the code says about itself.
Sit with that matching rule, because everything follows from it. The engine is not looking up "the payment step" by name. It is saying: this is the fourth operation this execution has requested; the history says the fourth result was such-and-such; here it is. Now what happens if the second run does not request the same fourth operation?
Try it concretely. Suppose the code says: if it is before noon, charge the card and then email; otherwise email and then charge. The first run happens at eleven, so operation one is the charge and operation two is the email. The machine dies. Recovery happens at one o'clock — and now the code takes the other branch, so operation one is the email. The engine dutifully answers it with the recorded result of the charge. The execution is now not merely wrong; it is wrong in a way that leaves no trace of having gone wrong.
Do you see that the clock was never the real problem? The real problem is any input that can differ between runs. The current time is the most common, but the family is larger: random numbers, a fresh UUID, an environment variable, the machine's hostname, a hash map whose iteration order the runtime does not fix, a concurrent race that resolves differently. Each can make run two diverge from run one.
So what is the fix? Notice we do not actually need to forbid these things — we need them to return the same answer on every run. And there is already a mechanism that makes an answer the same on every run: the history. So the engine intercepts them. Ask a workflow engine for the time and it does not call the operating system; it returns the time recorded for that point in the history. Ask for a random number and it gives you a seeded generator whose seed is part of the history. Ask to sleep for an hour and it records a timer rather than blocking. The call still appears in your code and still returns a sensible value — it has simply been rerouted through the record.
Anything that genuinely cannot be made repeatable is pushed outside the workflow. Calling a payment provider is not deterministic and never will be, so it does not run as workflow code at all: it runs as a separately recorded step, and on replay the workflow receives its stored result without the call being made. This is the split people mean by side-effect isolation — the workflow function contains only orchestration logic, which must be deterministic, and every interaction with the world is a recorded step, which need not be.
There is one further wrinkle worth naming honestly, because it surprises people in production rather than in tutorials. Determinism must hold not only across a crash but across a deployment. If a workflow has been running for three weeks and you ship a version with an extra step inserted before the existing ones, replay of the in-flight execution will request operations in an order the history does not match. Engines detect this and stop, and the accepted remedy is explicit versioning — a branch in the code that asks which version this particular execution began under — rather than trusting that the change was harmless.
The analogy
THE ANALOGY #Think of a stage play performed twice from the same script, where the second performance must reproduce the first exactly, line for line, because a prompter is feeding responses from a transcript of the first night. Anything in the script that says "improvise here" or "react to tonight's audience" wrecks it — not because improvisation is bad theatre, but because the prompter's transcript is indexed by line number, and one unscripted line shifts every subsequent cue by one.
an actor who deviates hears the wrong cue and notices, whereas replayed code has no idea the answer it was handed belongs to a different question — which is why the constraint has to be enforced by the engine rather than left to the author's care.
Clarifying the model
THE MODEL #A few refinements.
First, this is not a rule about purity in the functional sense. The workflow is allowed to have enormous effects on the world; it just has to have them through recorded steps rather than directly. The determinism requirement applies to the decision-making code, not to the system.
Second, the common misreading is that non-determinism will cause a wrong answer. It is worse than that: it causes a misaligned history, where every later result is attached to the wrong operation. That is why engines respond to detected divergence by halting rather than by continuing with a warning — there is nothing safe to continue into.
Third, the constraint is narrower than it first sounds. Ordinary computation, loops, arithmetic, string handling, business rules — all of that is unaffected, because it is a pure function of values the code already holds. What is restricted is precisely the set of calls that reach outside the execution for a value. Once you see the rule as "no unrecorded inputs" rather than "no side effects", the list of forbidden things stops feeling arbitrary.
A picture of it
THE PICTURE #How to readenter at the parallelogram, which is any value the code asks for. The two diamonds are the only questions that matter: could the answer change, and if so can it be captured. Both left-hand answers converge on the rounded outcome node, where replay retraces the original path. The lower branch is the one the rule exists to prevent: an uncaptured outside value leads to the risk node, where the history's answers silently attach to the wrong operations, and from there the only safe move is to stop.
What became clearer
WHAT CLEARED #The prohibition on reading the clock was never about the clock. Replay matches recorded results to operations by their position in a sequence, so the one thing the code must never do is take a different path the second time — and any value fetched from outside the execution can cause that. The engine's answer is not to remove those values but to record them, so that the second run asks the same questions in the same order and receives the same answers.
Where to go next
ONWARD #- How engines detect divergence, and why halting is safer than continuing.
- What versioning looks like for a workflow that has to change while instances of it are still in flight.
- Whether the same discipline is what makes event-sourced systems rebuildable from their logs.
Key terms
TERMS #| Term | What it means |
|---|---|
| Replay | re-running a workflow from its first line while answering completed operations from the recorded history. |
| Determinism | the property that the same code issues the same operations in the same order given the same answers. |
| Side-effect isolation | confining every interaction with the outside world to separately recorded steps, so the orchestrating code stays deterministic. |
| Non-determinism error | the engine's response when replay requests an operation the history does not match. |
Every term the collection defines is gathered in the glossary.