Replay, Explained
The previous lesson said the orchestration is stateless between cycles and can be torn down and resumed on another machine. That sounds like magic, and if you treat it as magic you'll eventually write orchestrator code that behaves bizarrely. It isn't magic — it's a specific, well-documented mechanism called replay, and because everything you can and can't do in an orchestrator follows from it, this lesson explains it the way Microsoft's own Durable Functions documentation does, in full.
The one idea to hold on to
Durable Functions does not snapshot your variables — it uses event sourcing. Instead of saving "the current state," the framework records an append-only history of every durable action the orchestrator takes, and it reconstructs state by replaying your function from the top against that history. Your orchestrator code is not run once; it is run many times, and each run rebuilds its progress by reading what already happened.
That single design choice is why an orchestration can survive a process crash, a deploy, or a scale-in event mid-loop: there is nothing in memory to lose. The truth lives in the history log, and the code is just a deterministic function of it.
How a replay actually proceeds
Walk one loop of a camera's orchestration as the framework sees it. The orchestrator awaits a
few durable operations — read the entity, capture an image, persist the new position, set a
timer — and here's what happens at each await:
- The orchestrator runs from the beginning.
- At an
awaiton a durable operation, the framework checks the history:- Already recorded as complete? It returns the saved result immediately and execution continues. The operation is not run again. This is "replay."
- Not yet in the history? This is new work. The framework schedules the operation, appends it to the history, and then checkpoints and unloads the orchestrator — the function stops executing and occupies no compute while it waits.
- When that operation completes, the orchestrator is re-executed from the very top. Every
prior operation replays from history (returning recorded results, never re-running); the
just-completed one now returns its result; execution advances to the next
await. - Steps 2–3 repeat until the orchestrator returns or calls
ContinueAsNew.
Step through it — watch which operations replay from history versus which is genuinely new each time the orchestrator wakes, and notice that between wakes it isn't sleeping in memory, it's gone:
First run. Nothing is in history yet, so “Read CameraState” is new work: the framework schedules it, writes it to the history, then checkpoints and UNLOADS the orchestrator. It is no longer running — it occupies no compute while it waits.
Three things that demo makes concrete are the real semantics: the orchestrator function is re-executed once per completed await (so it runs many times for one logical loop); prior operations are replayed from history, never re-run; and while it awaits, the orchestrator holds no compute — it's unloaded and only reawoken when there's a result.
The consequence: your code must be deterministic
If the framework re-executes your function again and again and expects it to line up with the recorded history, then every replay must make the same decisions given the same history. That's not a style preference — a non-deterministic orchestrator produces a history that diverges from the code, which the runtime detects and fails. This is the source of every "orchestrator code constraint," and it's why the engine is written the way it is:
| Don't, in orchestrator code | Because | Do instead |
|---|---|---|
DateTime.UtcNow | differs every replay | context.CurrentUtcDateTime (stable across replays) |
Guid.NewGuid(), random | differs every replay | pass values in, or generate inside an activity |
| Call a database / HTTP / camera directly | side effects would repeat, and results differ | do it in an activity, whose result is recorded once |
| Log normally | logs would duplicate on every replay | context.CreateReplaySafeLogger(...) |
Because the orchestrator re-executes on every wake, naive logging or side effects in orchestrator code appear to happen many times for one loop. That's replay doing its job, not a double-execution bug. Replay-safe logging suppresses the duplicates; a raw side effect in the orchestrator would genuinely repeat. If you see something happen "too many times," check whether it's an orchestrator side effect that belongs in an activity.
Keeping the history from growing forever
There's one loose end. If every durable operation appends to the history, an eternal loop —
which is exactly what a camera runs — would grow its history without bound, and every replay
would get slower as it re-walks more events. The fix is ContinueAsNew: at the end of each
cycle the orchestrator restarts itself with a fresh, empty history, carrying its input
forward. The eternal loop is really an unbroken chain of short orchestrations, each with a small
history. That mechanism, and the very real cost of the operations you put between the wakes, is
the next lesson.
From the Microsoft docs
Replay is a framework behavior, not an ALERTCalifornia invention — these are the authoritative references, worth reading once:
- Durable Functions overview
- Orchestrations & reliability (replay / event sourcing)
- Orchestrator function code constraints (determinism)
- Eternal orchestrations (
ContinueAsNew)
Wrapping up
Durable Functions preserves an orchestration not by saving memory but by event-sourcing its actions into a history and replaying the function from the top — returning recorded results for completed operations, scheduling the next new one, and unloading from compute while it waits. The whole set of orchestrator rules (deterministic time, no direct I/O, activities for side effects, replay-safe logging) exists to make that replay honest. Hold this model; the rest of the section is consequences of it.
Next, what replay costs once you understand it — and why lease mode pays the most: The Cost of Remembering.