Reported symptoms
At 22:14 on 2026-07-29 the primary site lost its upstream and the generator did
not pick up the load. The failover ran to plan: the standby at rbdr-site-b was
promoted at 22:31, DNS moved, and svc-orders was taking orders seventeen
minutes later. Nobody had a bad word for it.
The estate ran at rbdr-site-b for two days — orders taken, invoices issued,
dispatch notes printed, customers billed. On 2026-08-01 the primary was back and
the failback was executed between 09:41 and 10:05. Twenty-four minutes, which
everybody read as a sign the exercise had gone well.
At 14:20 finance asked why the order sequence had gone backwards, and why two customers invoiced on 31 July had no order on file. Every order created between the failover and the failback — 1,692 of them, identifiers 48214 to 49905 — is missing from the live database. Orders from before 22:14 on 29 July are all present. New orders are being issued identifiers already used during the two days at the recovery site, so the damage is still growing.
Evidence provided
$ psql -At -d rbdr_orders -c 'SELECT max(id), max(created_at) FROM orders'The primary ends at identifier 48213, created 2026-07-29 22:13:52 UTC. No partial row after it and no corruption: the clean stop of a database that was no longer being asked questions.
The recovery site kept its own archive. rbdr-site-b archived its
write-ahead log to rbdr-wal-b throughout, on timeline 2.
RBDR_ARCHIVE=/srv/rbdr-wal-b
RBDR_TIMELINE=00000002
cat "$RBDR_ARCHIVE/${RBDR_TIMELINE}.history"
find "$RBDR_ARCHIVE" -maxdepth 1 -name "${RBDR_TIMELINE}*" -printf '%f\n' \
| sort | tail -1
The history file records the fork point at which the two sides parted, and it is the only artefact in the estate that fixes that instant — no register, runbook or change record contains it. The segments on that timeline run from the promotion through 09:38 on 2026-08-01 and carry the commits for all 1,692 missing orders.
What the recovery site holds now. Its live data directory is a base backup
taken from rbdr-site-a on 2026-08-01, on timeline 1. The DR-era rows are not
there either.
The script that did it. The rebuild-standby log for 09:41 shows a base backup
taken from rbdr-site-a, the data directory at rbdr-site-b overwritten, exit 0.
Same script, same direction, as every day since the sites were built.
The plan. Section 8, Failover: twenty-four numbered steps, each with an owner and a stated verification. Section 9, Failback, in full: “When the primary site is available, reverse the steps in section 8.”
Work the evidence before reading on
- The primary’s data stops three minutes before the promotion; the recovery site’s continues two days past it. Which of those was anybody required to look at during the failback?
- Section 9 says to reverse section 8. Which of those twenty-four steps, reversed, moves a byte of order data northwards?
- The archive on timeline 2 survived. What does its existence rule out?
- Nobody can state the instant writes began at the recovery site. Name three decisions during the failback that needed that number.
Root cause
The primary was made authoritative and the delta was never moved
Failback consisted of starting the primary, running the estate’s rebuild-standby
script in the direction it had always run — rbdr-site-a to rbdr-site-b — and
moving DNS back. That script takes a base backup from the source and overwrites
the destination’s data directory: correct behaviour for the operation it was
written for, catastrophic for this one.
The primary held the state of 22:13:52 and nothing after, because from that instant it was asked nothing. The recovery site held that state plus two days of production. The failback moved the older copy over the newer one. No step transferred the delta the other way, because no step in the sequence was about data at all.
A plan with a failover procedure and no failback procedure
Twenty-four steps on one side, one sentence on the other. “Reverse the steps” is not a defined operation: failover moved a service onto a site holding a known-good copy, while failback reconciles two copies that have both moved on, and a procedure that never mentions data cannot be reversed into one that does.
The gap has a second face. The estate keeps no register of which site was
authoritative from which instant; the failover change record names a person and a
date, not the moment writes began at rbdr-site-b. Without that moment nobody can
size a delta, so nobody can notice one was never moved — and the operator who
finished in twenty-four minutes had no artefact to tell him he was twenty-four
minutes short.
Resolution
Freeze before you fix. The only surviving copy of the DR-era data is the recovery
site’s pre-fork base backup plus rbdr-wal-b, and both sit inside the reach of an
ordinary retention policy. Hold them, and stop the archive being rotated or
re-pointed while the incident runs.
Take the two instants from the database rather than from memory: the fork point
from 00000002.history, the last commit from the newest segment on timeline 2.
Then recover to an isolated host, alongside the live database rather than instead
of it — never over it.
RBDR_STAGE=/srv/rbdr-stage/pgdata
RBDR_ARCHIVE=/srv/rbdr-wal-b
tar -xf /srv/rbdr-base-b/rbdr-base-20260729.tar -C "$RBDR_STAGE"
cat >> "$RBDR_STAGE/postgresql.auto.conf" <<EOF
restore_command = 'cp $RBDR_ARCHIVE/%f %p'
recovery_target_timeline = '2'
EOF
touch "$RBDR_STAGE/recovery.signal"
Set the target timeline explicitly rather than relying on a default. Then enumerate the DR-era rows and hand the list to whoever owns the records.
RBDR_STAGE_PORT=5433
psql -p "$RBDR_STAGE_PORT" -At -d rbdr_orders -c \
"SELECT id, created_at FROM orders WHERE created_at >= '2026-07-29 22:31:00+00'"
Two questions are theirs: which side wins where a record exists on both, and what to do about identifiers issued twice — the primary’s sequence restarted at 48214, so every order since the failback collides with one taken during the DR period. Reconciliation reaches outside the database too: invoices were sent, payments captured, dispatch notes printed, and no row undoes any of that. Move the sequence beyond the highest identifier issued anywhere before normal operation resumes.
Verification
The recovered DR-era identifiers are compared with the live database by count and by identifier, with every difference itemised rather than absorbed. No identifier appears twice, and the sequence sits beyond the highest value issued at either site, confirmed by query rather than by assumption.
A named person from the business — not from infrastructure — confirms against records they chose themselves that a specific order, invoice and dispatch note created during the DR period are present and correct.
The rewritten section 9 names an authoritative side per dataset, a delta transfer with a stated direction, a reconciliation step and a validation against DR-era records; the register carries a row for this incident whose failover instant came from the timeline history file.
Prevention
Write the failback procedure before the failover, with its own approver. “Reverse the failover steps” is not a procedure, and the second operation is the harder of the two.
Keep a register of which site is authoritative from which instant. Without it no delta can be sized, and an unsized delta is one nobody notices was skipped.
Make any tool that overwrites a data directory refuse when the destination holds history the source does not. The script did what it was written to do; nothing between the operator and the overwrite compared the two sides.
Validate a failback against DR-era records chosen by somebody else. Green dashboards were true at 10:05 and stayed true for four hours; a named order that exists only because the recovery site served it fails immediately.
Treat the recovery site as production while it is production. For two days it held data with no second home, and the archive that saved this incident existed by configuration inheritance rather than by decision.
Rehearse the failback, not only the failover. The unrehearsed half is the one executed after the bridge call has dissolved, by people who are tired, with nobody watching.