MSE907 failover study
Results
On this pageAnalysis dashboardAll 40 runsFindings and caveats
Results
Analysis dashboard
Every number here is computed in your browser from the 40 run records. Choose a fault and a direction, then pick a metric. The statistics match the analysis workflow: exact two-sided Wilcoxon signed-rank on matched pairs.
One metric, run by run
Seconds from the fault to the first payment the client sees succeed.
All metrics for this selection
| Metric | Pairs | A median | B median | Median diff (A − B) | Hodges–Lehmann | Rank-biserial r | W | p (exact) |
|---|---|---|---|---|---|---|---|---|
| Client RTO (s) | 10 | 31.1 | 181.7 | -150.9 | -151.1 | -1.00 | 0.0 | 0.0020 |
| Steady RTO (s) | 10 | 37.9 | 240.9 | -203.6 | -203.6 | -1.00 | 0.0 | 0.0020 |
| Platform: new primary writable (s) | 10 | 11.4 | 142.9 | -131.9 | -131.9 | -1.00 | 0.0 | 0.0020 |
| Platform: DNS switched (s) | 10 | 11.9 | 143.5 | -132.0 | -131.9 | -1.00 | 0.0 | 0.0020 |
| Acknowledged payments lost (at the end) | 10 | 0.0 | 630.0 | -630.0 | -632.5 | -1.00 | 0.0 | 0.0020 |
| RPO: payments lost at recovery | 10 | 0.0 | 630.0 | -630.0 | -632.5 | -1.00 | 0.0 | 0.0020 |
| RPO window (s) | 10 | 0.0 | 125.8 | -125.8 | -126.2 | -1.00 | 0.0 | 0.0020 |
| Failed attempts after first recovery | 10 | 7.0 | 2,357.0 | -2,351.0 | -2,360.0 | -1.00 | 0.0 | 0.0020 |
| Attempts never acknowledged | 10 | 0.0 | 454.5 | -454.5 | -456.5 | -1.00 | 0.0 | 0.0020 |
Two-sided Wilcoxon signed-rank test on matched pairs, zero differences dropped, p computed exactly. Differences are A − B, so negative means A is lower. Rank-biserial r of −1 means A was lower in every pair. A pair is left out of a metric when either run has no value for it.
Incident rate
| Outcome | A runs | A rate (95% CI) | B runs | B rate (95% CI) | A relative to B |
|---|---|---|---|---|---|
| Lost | 0 of 10 | 0% (0 to 31) | 10 of 10 | 100% (69 to 100) | 100% fewer |
| Duplicated | 0 of 10 | 0% (0 to 31) | 0 of 10 | 0% (0 to 31) | – |
| Delayed but recovered | 0 of 10 | 0% (0 to 31) | 0 of 10 | 0% (0 to 31) | – |
| Incident (lost or duplicated) | 0 of 10 | 0% (0 to 31) | 10 of 10 | 100% (69 to 100) | 100% fewer |
Clopper–Pearson intervals. Exact McNemar test on the 10 discordant pairs (only A: 0, only B: 10): p = 0.0020. Payments lost or duplicated per 1,000 acknowledged: A 0.00 (0 of 10,500), B 281.00 (6,334 of 22,541).
The four cells side by side
| Fault | Direction | A client RTO | B client RTO | A steady | B steady | A lost | B lost | B loss window (s) |
|---|---|---|---|---|---|---|---|---|
| Pod-kill | AKS to EKS | 31.1 | 181.8 | 39.2 | 240.8 | 0 | 619 | 123.5 |
| Pod-kill | EKS to AKS | 30.8 | 181.7 | 31.4 | 243.0 | 0 | 645 | 128.8 |
| Partition | AKS to EKS | 22.6 | 172.7 | 22.6 | 230.7 | 0 | 682 | 136.1 |
| Partition | EKS to AKS | 77.6 | 166.4 | 110.1 | 228.6 | 0 | 746 | 148.9 |
Medians over five runs per condition (four for A in EKS to AKS pod-kill, where one run has no recovery time). With only five pairs, a single cell can never reach an exact p below 0.0625; the tests above pool the directions to reach 10 pairs per fault.
Results
All 40 runs
| A-podkill-01 | A | pod-kill | aks-to-eks | 1 | 25 Oct, 15:53 | 31.3 | 38.0 | 9.9 | 10.3 | 0 | 0 | 1,050 | 5 |
| B-podkill-01 | B | pod-kill | aks-to-eks | 1 | 25 Oct, 16:19 | 182.4 | 244.3 | 138.8 | 139.3 | 619 | 123.5 | 2,210 | 2,412 |
| B-podkill-02 | B | pod-kill | aks-to-eks | 2 | 25 Oct, 16:43 | 181.8 | 241.1 | 166.6 | 167.0 | 621 | 123.9 | 2,255 | 2,258 |
| A-podkill-02 | A | pod-kill | aks-to-eks | 2 | 25 Oct, 17:02 | 31.2 | 42.5 | 9.7 | 10.3 | 0 | 0 | 1,050 | 28 |
| A-podkill-03 | A | pod-kill | aks-to-eks | 3 | 25 Oct, 17:19 | 31.1 | 38.7 | 8.9 | 9.4 | 0 | 0 | 1,050 | 12 |
| B-podkill-03 | B | pod-kill | aks-to-eks | 3 | 25 Oct, 17:44 | 181.7 | 240.8 | 154.5 | 154.8 | 621 | 123.9 | 2,246 | 2,373 |
| B-podkill-04 | B | pod-kill | aks-to-eks | 4 | 25 Oct, 18:10 | 182.8 | 239.3 | 145.3 | 145.7 | 618 | 123.3 | 2,231 | 2,226 |
| A-podkill-04 | A | pod-kill | aks-to-eks | 4 | 25 Oct, 18:27 | 31.1 | 41.0 | 9.1 | 9.5 | 0 | 0 | 1,050 | 9 |
| A-podkill-05 | A | pod-kill | aks-to-eks | 5 | 25 Oct, 18:44 | 31.1 | 39.2 | 8.5 | 9.0 | 0 | 0 | 1,050 | 14 |
| B-podkill-05 | B | pod-kill | aks-to-eks | 5 | 25 Oct, 19:08 | 181.3 | 238.7 | 142.0 | 142.4 | 617 | 123.1 | 2,248 | 2,239 |
| A-partition-01 | A | partition | aks-to-eks | 1 | 25 Oct, 19:27 | 21.8 | 21.8 | 12.8 | 13.3 | 0 | 0 | 1,050 | 0 |
| B-partition-01 | B | partition | aks-to-eks | 1 | 25 Oct, 19:53 | 172.7 | 230.7 | 158.9 | 159.4 | 682 | 136.1 | 2,323 | 2,248 |
| B-partition-02 | B | partition | aks-to-eks | 2 | 25 Oct, 20:16 | 172.2 | 231.4 | 157.7 | 158.1 | 667 | 133.1 | 2,294 | 2,256 |
| A-partition-02 | A | partition | aks-to-eks | 2 | 25 Oct, 20:35 | 23.7 | 23.7 | 11.9 | 12.3 | 0 | 0 | 1,050 | 0 |
| A-partition-03 | A | partition | aks-to-eks | 3 | 25 Oct, 20:52 | 22.2 | 22.2 | 12.2 | 12.7 | 0 | 0 | 1,050 | 0 |
| B-partition-03 | B | partition | aks-to-eks | 3 | 25 Oct, 21:16 | 173.8 | 231.5 | 143.7 | 144.0 | 667 | 133.2 | 2,284 | 2,279 |
| B-partition-04 | B | partition | aks-to-eks | 4 | 25 Oct, 21:41 | 172.7 | 230.2 | 166.6 | 167.5 | 685 | 136.8 | 2,313 | 2,250 |
| A-partition-04 | A | partition | aks-to-eks | 4 | 25 Oct, 21:59 | 75.6 | 111.3 | 11.2 | 11.7 | 0 | 0 | 1,050 | 1,424 |
| A-partition-05 | A | partition | aks-to-eks | 5 | 25 Oct, 22:17 | 22.6 | 22.6 | 8.9 | 9.4 | 0 | 0 | 1,050 | 0 |
| B-partition-05 | B | partition | aks-to-eks | 5 | 25 Oct, 22:41 | 172.2 | 226.9 | 167.0 | 167.4 | 684 | 136.5 | 2,309 | 2,111 |
| B-podkill-06 | B | pod-kill | eks-to-aks | 6 | 25 Oct, 23:27 | 181.5 | 237.9 | 143.9 | 144.6 | 654 | 130.6 | 2,273 | 2,341 |
| B-podkill-07 | B | pod-kill | eks-to-aks | 7 | 25 Oct, 23:50 | 184.2 | 245.0 | 133.2 | 133.6 | 645 | 128.8 | 2,232 | 2,404 |
| A-podkill-07 | A | pod-kill | eks-to-aks | 7 | 26 Oct, 00:08 | 30.8 | 36.4 | 14.8 | 15.2 | 0 | 0 | 1,050 | 5 |
| A-podkill-08 | A | pod-kill | eks-to-aks | 8 | 26 Oct, 00:25 | 30.8 | 30.8 | 13.0 | 13.4 | 0 | 0 | 1,050 | 0 |
| B-podkill-08 | B | pod-kill | eks-to-aks | 8 | 26 Oct, 00:49 | 147.7 | 205.1 | 131.1 | 131.5 | 644 | 128.5 | 2,424 | 2,313 |
| B-podkill-09 | B | pod-kill | eks-to-aks | 9 | 26 Oct, 01:14 | 183.7 | 250.3 | 126.2 | 126.6 | 639 | 127.6 | 2,177 | 3,022 |
| A-podkill-09 | A | pod-kill | eks-to-aks | 9 | 26 Oct, 01:31 | 31.4 | 31.4 | 15.9 | 16.4 | 0 | 0 | 1,050 | 0 |
| A-podkill-10 | A | pod-kill | eks-to-aks | 10 | 26 Oct, 01:48 | 31.2 | 37.9 | 14.3 | 14.7 | 0 | 0 | 1,050 | 12 |
| B-podkill-10 | B | pod-kill | eks-to-aks | 10 | 26 Oct, 02:13 | 181.7 | 243.0 | 156.8 | 157.3 | 656 | 130.9 | 2,245 | 2,600 |
| A-partition-06 | A | partition | eks-to-aks | 6 | 26 Oct, 02:32 | 78.0 | 108.6 | 17.9 | 18.3 | 0 | 0 | 1,050 | 1,093 |
| B-partition-06 | B | partition | eks-to-aks | 6 | 26 Oct, 02:57 | 166.1 | 228.6 | 147.2 | 147.8 | 751 | 149.8 | 2,360 | 2,572 |
| B-partition-07 | B | partition | eks-to-aks | 7 | 26 Oct, 03:21 | 166.4 | 229.1 | 160.2 | 160.6 | 746 | 148.8 | 2,349 | 2,717 |
| A-partition-07 | A | partition | eks-to-aks | 7 | 26 Oct, 03:39 | 77.6 | 112.3 | 17.8 | 18.4 | 0 | 0 | 1,050 | 1,398 |
| A-partition-08 | A | partition | eks-to-aks | 8 | 26 Oct, 03:56 | 76.7 | 110.1 | 19.3 | 19.7 | 0 | 0 | 1,050 | 1,384 |
| B-partition-08 | B | partition | eks-to-aks | 8 | 26 Oct, 04:20 | 165.5 | 230.4 | 146.8 | 147.3 | 751 | 149.9 | 2,319 | 2,844 |
| B-partition-09 | B | partition | eks-to-aks | 9 | 26 Oct, 04:44 | 166.5 | 225.7 | 148.9 | 149.6 | 746 | 148.9 | 2,321 | 2,838 |
| A-partition-09 | A | partition | eks-to-aks | 9 | 26 Oct, 05:02 | 76.9 | 105.6 | 19.5 | 20.0 | 0 | 0 | 1,050 | 985 |
| A-partition-10 | A | partition | eks-to-aks | 10 | 26 Oct, 05:20 | 77.6 | 112.7 | 18.4 | 18.9 | 0 | 0 | 1,050 | 1,406 |
| B-partition-10 | B | partition | eks-to-aks | 10 | 26 Oct, 05:45 | 167.9 | 223.5 | 148.5 | 149.0 | 732 | 146.2 | 2,365 | 2,237 |
| A-podkill-06-rerun | A | pod-kill | eks-to-aks | 6 | 26 Oct, 20:08 | 25.0 | 25.0 | 13.6 | 14.0 | 0 | 0 | 1,050 | 0 |
40 of 40 runs. Click a row for the remaining fields. Times are New Zealand time.
Results
Findings and caveats
What the 40 runs support
- H1, incident rate. A lost no acknowledged payment in any run. B lost between 617 and 751 payments in every run. Nothing was duplicated anywhere, as designed.
- H2, time. A’s median client RTO is lower than B’s under both faults. The gap is large for a pod-kill (about 31 s against 182 s) and smaller for a partition (pooled medians 76 s against 170 s). The pooled partition median for A sits between two clusters: about 22 s in AKS to EKS and about 77 s in EKS to AKS (see the first caveat below).
- H4, direction. In all four cells A is better than B on payment loss and on client RTO. The sign does not depend on direction. The size of A’s advantage does.
What they do not support yet
- H3, replay. The 40 runs cannot show it: no condition A run ever had a gap to replay. It is tested separately by the ten-pair H3 test below, which does create a real gap. Every lost B payment was still in the change log, so reconciliation could have recovered them, but the 40 runs themselves do not demonstrate that.
- What the 40 runs do and do not show about the reconciler. They show that condition A preserved every acknowledged payment under the tested faults. They do not show that the reconciliation mechanism is the reason, because live replication had already copied each payment, so there was nothing for the reconciler to recover (
missing_beforewas 0). The result compares the whole approach, replication plus orchestration, against backup and restore. - Per-direction significance. Each cell has five pairs, so its exact p cannot be below 0.0625.
The H3 test: replay of lost writes
Status: ten matched pairs done. All ten pairs ran on 26 October 2026 on the same AKS and EKS platform as the 40 runs, in four chunks of one workflow, all in the AKS-to-EKS direction. With replay on, every pair passed: 10 of 10, exact McNemar p = 0.0020, 95% Clopper–Pearson interval 69% to 100%. The two earlier trial runs are shown in the diagram and are not part of the ten pairs.
- Warm up as in every run. Then cut replication to the standby for 15 seconds with two Chaos Mesh partitions scoped to the standby database pod, so that the change-log connector still reads the primary: one stops the standby pod sending to the tunnel gateway, the other stops the gateway sending the write-ahead log to the standby pod.
- During those 15 seconds about 75 payments are acknowledged on the primary only. They are also in the Kafka change log. A lookup through the standby’s own bank app records how many are missing there (the gap).
- Kill the primary pod, as in the pod-kill runs. The controller detects it, moves the lease, fences the old primary, promotes the standby and switches DNS. The standby is promoted without the gap payments.
- The reconciler reads the change log and replays the missing payments through the bank app with their original payment ids (the idempotency key). The failed site then returns and the reconciler runs again.
Each pair is two runs with the same load: H3-A-NN with replay on, and H3-control-NN with replay off, where the reconciler only classifies and the same payments should stay lost. Pass criteria for a replay run: a gap above 0, no acknowledged payment missing at the end, no duplicate, and at least the gap recovered by replay. Ten pairs allow an exact McNemar p of 0.0020 and a 95% lower bound of 69% on the pass rate. The test is a condition A run in a new fault mode, so it is a separate analysis and does not change the 40-run results. How the controller behaves when the standby is cut off at promotion time is already seen in the partition runs, where replication was cut; in both trial runs the cut held until the standby was promoted.
One run, step by step
1. Normal running. The primary in AKS takes payments. Its writes stream to the standby in EKS, and Debezium records each change in Kafka.
The numbers are from two trial runs (trial-h3-01 with replay on, trial-h3-02 with replay off), done before the ten pairs. They are not part of the pair statistics; the ten pairs are in the table below.
| From | To | What travels | How |
|---|---|---|---|
| Load generator | Cloudflare DNS | The client looks up the bank name. | DNS |
| Load generator | Bank app (AKS) | Payments go to the primary (AKS). | HTTPS |
| Bank app (AKS) | PostgreSQL (AKS) | Each payment and its receipt are written to the database. | PostgreSQL |
| PostgreSQL (AKS) | WireGuard (AKS) | The primary streams its write-ahead log. | PostgreSQL streaming |
| WireGuard (AKS) | WireGuard (EKS) | Through the WireGuard tunnel to the other cloud. | WireGuard |
| WireGuard (EKS) | PostgreSQL (EKS) | The standby applies it and so follows the primary. | PostgreSQL streaming |
| PostgreSQL (AKS) | WireGuard (AKS) | Debezium reads the primary’s changes. | logical decoding |
| WireGuard (AKS) | WireGuard (EKS) | Through the same tunnel and gateway. | WireGuard |
| WireGuard (EKS) | Debezium (EKS) | They reach Debezium on the standby side. | TCP 5432 via the gateway service |
| Debezium (EKS) | Kafka (EKS) | Each change is stored in Kafka: a second record of every payment. | Kafka |
In the trial with replay on, the gap at the fault was 75 payments; the reconciler replayed 98 (the gap plus payments acknowledged before the pod died), and no payment was lost or duplicated. With replay off the gap was 77 and 102 acknowledged payments stayed lost, although all of them were still in Kafka. These two runs are a demonstration, not a result: the ten pairs below are what the statistics use.
The ten pairs
Each row is one matched pair: the replay-on run and the replay-off control with the same seed and load, AKS to EKS. “Gap” is the number of acknowledged payments missing on the standby just before the primary was killed. “Recovered” is how many the reconciler wrote back (replay on); “lost” is how many acknowledged payments are missing at the end.
| Pair | Gap (on) | Recovered (on) | Lost (on) | Duplicated (on) | RTO on (s) | Gap (control) | Lost (control) | RTO control (s) |
|---|---|---|---|---|---|---|---|---|
| 1 | 76 | 99 | 0 | 0 | 61.6 | 77 | 107 | 57.3 |
| 2 | 76 | 98 | 0 | 0 | 62.2 | 76 | 105 | 56.9 |
| 3 | 75 | 104 | 0 | 0 | 59.8 | 76 | 97 | 61.4 |
| 4 | 75 | 97 | 0 | 0 | 62.7 | 76 | 104 | 58.2 |
| 5 | 76 | 101 | 0 | 0 | 60.4 | 77 | 107 | 56.0 |
| 6 | 75 | 102 | 0 | 0 | 59.8 | 75 | 100 | 59.7 |
| 7 | 76 | 107 | 0 | 0 | 57.1 | 76 | 106 | 58.4 |
| 8 | 77 | 101 | 0 | 0 | 62.4 | 76 | 100 | 61.9 |
| 9 | 77 | 105 | 0 | 0 | 59.7 | 76 | 97 | 61.2 |
| 10 | 77 | 103 | 0 | 0 | 51.9 | 76 | 107 | 57.2 |
Result. With replay on, every acknowledged payment was present at the end in all ten runs, none was duplicated, and the reconciler recovered at least the gap each time (1,017 payments recovered against a combined gap of 760). With replay off, the same kind of payments stayed lost in all ten runs (1,030 lost against a combined gap of 761), although all of them were still in the Kafka change log. In every pair the replay run recovered and the control lost; there was no pair where the control did better, so the exact McNemar test gives p = 0.0020, the smallest p that ten pairs allow. The pass rate for replay is 10 of 10, with a 95% Clopper–Pearson interval of 69% to 100%. H3 is supported for this fault and direction.
Why recovered and lost exceed the gap. The gap is measured shortly before the primary is killed. Payments acknowledged between that measurement and the pod dying are also missing on the new primary, so the reconciler replays them too, and the control loses them too. That is why 97 to 107 payments are recovered (or lost) against a gap of 75 to 77.
What this does not show. It is one fault (a 15 s replication cut followed by a pod-kill) in one direction (AKS to EKS), with a replication gap that the experiment creates on purpose; it does not show how often such gaps occur in real use. Recovery time was about 60 s in the replay runs (median 60.1 s) and about 58 s in the controls (median 58.3 s). RTO is not what H3 tests, and this difference was not tested. It is longer than the 31 s of the 40 pod-kill runs; the cause was not investigated. Results were produced by the author's own harness and the ten pairs are not blinded.
Two things in the data to read carefully
1. A’s EKS-to-AKS partition recovery is slow in every run. The client first succeeds at 76.7 to 78.0 s in all five runs, against about 22 s in AKS to EKS. The platform had switched DNS at 18 to 20 s, so the client waited about 58 s more, which is close to the full 60 s DNS TTL. That fits a cached answer. It has not been tested. It lowers A’s advantage in that cell (77.6 s against 166.4 s) but does not remove it. In AKS to EKS one partition run also relapsed (75.6 s, steady 111 s).
2. One A run in EKS-to-AKS pod-kill (A-podkill-06) had to be repeated. In the first attempt (25 October) the fault did not take effect: the controller stayed Healthy at lease epoch 1 with no failover, all 1,050 payments succeeded first time, and reconciliation never ran, so the run is not a failover measurement and was dropped. The result folder also held stale reconciliation and partition files from 24 October, and no fault log, so the exact reason the kill did not fire is not proven. The run was repeated on 26 October as A-podkill-06-rerun (EKS to AKS, pod-kill, condition A, pair 6, same load seed 1006) and replaces it in all figures. In the repeat the controller failed over (lease epoch 2, writable at 13.6 s, DNS switched at 14.0 s), the client first succeeded at 25.0 s, and no payment was lost or duplicated. Two limits to keep in mind. The repeat ran on a rebuilt copy of the platform, made from the same code and configuration on 26 October, so it is not from the same deployment as the other 39 runs. And it was run because the first attempt failed, so it is not an independent draw; the failure was in the fault injection, not in the outcome. Its recovery time (25.0 s) is about 6 s lower than the other nine condition A pod-kill runs (30.8 to 31.4 s); the client switched to the new address at 24.3 s, and the cause of the difference was not investigated. Pod-kill RTO therefore uses all 10 pairs. The original run’s zip is kept for reference.
Other observations
- B’s steady RTO exceeds its first-success RTO by about 58 to 62 s in the cell medians: the restored standby fails again under the retry backlog.
- B’s EKS-to-AKS pod-kill run 8 recovered faster than the rest (147.7 s against about 182 s).
- The platform phases show the same pattern as the clients: A’s new primary is writable at 8.5 to 19.5 s, B’s at 126 to 167 s.