Replay from log and the catch-up condition
When does replaying a log rebuild the same state, and how much headroom does catching up need?
What it is. State can be written as a fold over the log: state = fold(f, initial, log), meaning: start from an initial state and apply the function f to every log record in order. If the log is complete and ordered, any state you can reach by processing the log you can reach again by processing it again from a known offset. Replay does not need to know what the failure was, which makes it the cheapest mechanism to have. Its steady-state cost is a log you were already keeping.
Determinism is the precondition, and nobody checks it
Replay gives the same state only if f is a real function (same input, same output) and the log is its only input. The ways to break this are ordinary:
- reading the clock:
now(), - unseeded random numbers,
- enrichment lookups against a table that has changed since,
- summing floating-point numbers in a different partition order (floating-point addition is not associative: (a + b) + c can differ slightly from a + (b + c)),
- hash iteration order,
- and the subtle one: any dependence on the arrival order of events when that order is not itself recorded in the log.
The worst feature of this failure is that replay produces a different but plausible answer, silently, and only on the recovery path. The difference may surface weeks later as a reconciliation break. This is the clearest example here of a mechanism that assumes purity, cannot check purity, and lets the data pay without anyone filing a ticket. It is the third symptom: Wanjiru's code read now() and a lookup table that changed.
The catch-up condition
During an outage, new work keeps arriving. Recovery time is:
- the system's maximum throughput
- normal arrival rate
- how long the outage lasted
Data table
| Utilization a/M | Catch-up time ÷ outage |
|---|---|
| 0.05 | 0.053× |
| 0.061 | 0.065× |
| 0.073 | 0.078× |
| 0.084 | 0.091× |
| 0.095 | 0.105× |
| 0.106 | 0.119× |
| 0.117 | 0.133× |
| 0.129 | 0.148× |
| 0.14 | 0.163× |
| 0.151 | 0.178× |
| 0.163 | 0.194× |
| 0.174 | 0.21× |
| 0.185 | 0.227× |
| 0.196 | 0.244× |
| 0.207 | 0.262× |
| 0.219 | 0.28× |
| 0.23 | 0.299× |
| 0.241 | 0.318× |
| 0.253 | 0.338× |
| 0.264 | 0.358× |
| 0.275 | 0.379× |
| 0.286 | 0.401× |
| 0.297 | 0.423× |
| 0.309 | 0.447× |
| 0.32 | 0.471× |
| 0.331 | 0.495× |
| 0.342 | 0.521× |
| 0.354 | 0.547× |
| 0.365 | 0.575× |
| 0.376 | 0.603× |
| 0.387 | 0.633× |
| 0.399 | 0.663× |
| 0.41 | 0.695× |
| 0.421 | 0.728× |
| 0.432 | 0.762× |
| 0.444 | 0.798× |
| 0.455 | 0.835× |
| 0.466 | 0.874× |
| 0.477 | 0.914× |
| 0.489 | 0.956× |
| 0.5 | 1× |
| 0.511 | 1.05× |
| 0.522 | 1.09× |
| 0.534 | 1.14× |
| 0.545 | 1.2× |
| 0.556 | 1.25× |
| 0.568 | 1.31× |
| 0.579 | 1.37× |
| 0.59 | 1.44× |
| 0.601 | 1.51× |
| 0.612 | 1.58× |
| 0.624 | 1.66× |
| 0.635 | 1.74× |
| 0.646 | 1.83× |
| 0.657 | 1.92× |
| 0.669 | 2.02× |
| 0.68 | 2.12× |
| 0.691 | 2.24× |
| 0.703 | 2.36× |
| 0.714 | 2.49× |
| 0.725 | 2.64× |
| 0.736 | 2.79× |
| 0.748 | 2.96× |
| 0.759 | 3.15× |
| 0.77 | 3.35× |
| 0.781 | 3.57× |
| 0.792 | 3.82× |
| 0.804 | 4.1× |
| 0.815 | 4.41× |
| 0.826 | 4.76× |
| 0.837 | 5.15× |
| 0.849 | 5.61× |
| 0.86 | 6.14× |
| 0.871 | 6.77× |
| 0.882 | 7.51× |
| 0.894 | 8.41× |
| 0.905 | 9.53× |
| 0.916 | 10.9× |
| 0.927 | 12.8× |
| 0.939 | 15.3× |
| 0.95 | 19× |
If the replay throughput does not exceed the arrival rate, recovery never ends.
To recover in T/k you need the drain rate to be k times the arrival rate: M − a = k·a, so M = (k + 1)·a. To recover in a third of the outage (k = 3) you need maximum throughput four times the arrival rate, which means running at 25% utilization.
The consequence is important: the utilization target of a system that recovers by replay is set by its recovery budget, not by its latency budget. This is a different and usually stricter limit than the queueing argument (which says that latency grows quickly as utilization approaches 100%). It is also why Wanjiru's backlog barely shrank: results-intake ran at 90% of its maximum, so its drain rate was only 10% of maximum throughput (about 1/9 of the arrival rate). (The case where catch-up load breaks the system again is a replay storm.)
What bounds replay
The retention window: you can replay exactly as far back as someone decided to keep the log. So the retention policy is silently a recovery-capability policy, usually owned by a different team on cost grounds.