Australian cloud for business software — built and run in Sydney since 2003

NetLeverage
NetLeverage

Cutover runbook: replication lag, DNS TTL, validation gates and a real rollback

Technical article
Cloud

About this article: AI-assisted writing, reviewed against the cited sources. This is source research, not a lab validation of the procedures. Check the stated versions and your own recovery requirements before making changes.

A cutover plan needs evidence for four linked decisions: writes have stopped on the source, the target has reached a recorded position, DNS moves clients inside a window you calculated, and each stage has a stated rollback. Returning to an older source loses unreconciled target writes.

Identifiers and timings are illustrative.

Interpretations are editorial; no lab testing was performed.

Why it matters

Rollback must account for target writes. Zero lag and a short DNS TTL need context before they can support a cutover decision.

How it works

Lag: measure a position, not a feeling

A target can have received everything and still be applying it, and a time-based lag figure is an estimate. For a cutover, freeze writes, record the source's log position, and wait until the target has applied at least that position. That is the proof.

PostgreSQL

pg_stat_replication reports sent_lsn, replay_lsn, write_lag, flush_lag and replay_lag for each directly connected standby. An LSN (log sequence number) is a position in the write-ahead log, and pg_wal_lsn_diff returns the byte difference between two positions.

The time columns are not the proof. PostgreSQL says that when a standby has caught up and there is no more WAL activity, the last measured lag stays visible briefly and then shows NULL. With logical replication, a decoding plugin that emits no tracking messages also shows NULL. So NULL can appear in a healthy state and in an unmeasured one.

Use the position test after the freeze:

-- 1. On the primary, after all writers have stopped. Record the result.
SELECT pg_current_wal_lsn();

-- 2. On the standby. Replace the literal with the value from step 1.
SELECT pg_last_wal_replay_lsn(),
       pg_wal_lsn_diff(pg_last_wal_replay_lsn(), '0/5A000110') AS past_freeze_bytes;

For a confirmed physical standby of this source on the expected timeline, the position condition is past_freeze_bytes of zero or more. Verify that identity first; LSNs from unrelated histories are not comparable proof. Use "at least" and not "equal", because background activity can move the primary's position after your reading.

This test is for physical standbys only. pg_last_wal_replay_lsn returns NULL on a server started normally without recovery, which describes a logical replication subscriber. A marker row alone is not a complete logical-replication consistency proof. Use the version-specific subscription state and application boundaries, including parallel transactions and objects outside replication; this article does not define that procedure.

Confirm that the writers have really gone. pg_stat_activity has backend_type, client_addr and state. Only superusers and roles with pg_read_all_stats see every session's detail, so filter on backend_type = 'client backend' using one of those.

default_transaction_read_only sets only the default for each new transaction. Treat it as a guard against accidents and stop the applications as well. If you set it, the session writing the marker must first run SET default_transaction_read_only = off for itself.

Promotion uses pg_ctl promote or pg_promote(). pg_promote returns false if promotion did not finish within wait_seconds, so a script must check the result. At failover the standby restores WAL already available and makes no attempt to connect to the primary, so anything not received is left behind.

SQL Server Always On

Query sys.dm_hadr_database_replica_states on the primary, which reports every replica. Join sys.availability_replicas on replica_id if you want server names:

SELECT DB_NAME(database_id) AS database_name, replica_id, is_local,
       is_primary_replica, synchronization_state_desc,
       log_send_queue_size, redo_queue_size,
       last_commit_lsn, last_commit_time, secondary_lag_seconds
FROM sys.dm_hadr_database_replica_states
ORDER BY database_name, is_primary_replica DESC;

The fields mean different things:

Column What it tells you Trap
log_send_queue_size Log not yet sent to the secondary, in KB This is the data a catastrophic source failure could lose
redo_queue_size Log received on the secondary and not yet redone, in KB Adds to failover time, not to data loss
last_commit_lsn, last_commit_time Last commit position and time Compare primary and secondary rows
secondary_lag_seconds Seconds the secondary is behind (SQL Server 2016 and later) Shows 0 if data movement is suspended

A suspended secondary can report zero seconds of lag, so always read synchronization_state_desc beside it. Microsoft also warns that some values may be unavailable or out of date, and that they have only local relevance.

Our pass condition after the freeze: every database on the target shows SYNCHRONIZED, log_send_queue_size is 0, and last_commit_lsn on the secondary row equals the primary row.

A planned manual failover without data loss is supported only when both replicas are in synchronous-commit mode and the target is synchronised. Check is_failover_ready in sys.dm_hadr_database_replica_cluster_states, use the supported planned-failover procedure for that group type, validated in rehearsal.

The command returns once the target has accepted it, and database recovery continues asynchronously, so the next gate must check that databases are online. A read-scale group with CLUSTER_TYPE = NONE has its own failover procedure, so this command is not the method there. And consistency across several databases in the group "might not be maintained on failover", so check any application that spans databases.

SQL Server log shipping or manual log restores

There is no failover command here. Microsoft's controlled failover sequence is: copy any uncopied backups, apply them in order, take a tail-log backup, apply it, then recover the secondary. Microsoft notes you may need single-user mode for exclusive access before the restore. Its role-change procedure also says to disable the backup job on the original primary and the copy and restore jobs on the secondary, so include those jobs in the freeze.

The tail-log step is a planned outage: the NORECOVERY option leaves the source database in RESTORING and unavailable. Have the DBA implement the version-specific backup and restore procedure, with exclusive-access requirements, verified backup destinations and a rehearsed return path.

WITH NORECOVERY guarantees the database does not change after the tail-log backup. It also keeps the way back open, because Microsoft says you can later roll this database forward with log backups from the replacement primary. Microsoft also tells you to make the secondary's metadata consistent with the original primary when you bring it online.

The pass condition here is that every log backup, including the tail, restored without error and in order, and that the database came online after recovery with the marker row present. A secondary held in NORECOVERY cannot be read before that.

Whole-VM replication

Machine-level tools report their own lag, and it does not say whether the application was quiet.

AWS Transform MGN describes Lag as how far behind an instance launched now would be (the page says "recovery instance"), and Backlog as data written at the source and not yet copied. Its readiness indicators are the lifecycle state Ready for cutover and a Healthy replication status. All four are the pass condition.

Azure Migrate agentless replication runs an on-demand final delta when you migrate. Its concepts page says you must shut down the on-premises VM first to prevent data loss; the tutorial makes that a choice for "a planned migration with no data loss". Treat it as required. Complete Migration will also "clean up resources and shut down the source virtual machines", so do not run it while the source is your way back. The tutorial's test migration is the natural rehearsal.

Where a database inside the VM is replicated separately, use the database proof and treat the machine's lag as secondary evidence.

DNS: the TTL you must wait out is the old one

A resolver can keep an answer for the TTL it received. Lowering the TTL affects only answers fetched after the change. A resolver that fetched the record just before you lowered it keeps the old answer for the full old TTL.

RFC 1034 states the method: reduce the TTL before an anticipated change, then raise it afterwards.

Step Example with an old TTL of 86,400 seconds
Lower TTL to 300 At least 24 hours before cutover, counted from when the last authoritative server is confirmed serving 300. Allow 48 hours for margin
Confirm Every authoritative server returns TTL 300
Change the address At the planned step
Conforming caches move Within about 300 seconds of the change
Raise the TTL Only after the rollback window has closed

Query each authoritative server directly, because a recursive answer tells you only about that resolver's cache:

dig @ns1.example.net app.example.com A +noall +answer

Caches that do not behave like the TTL table

Negative answers. A resolver may cache "this name does not exist" for the lower of the SOA MINIMUM field and the SOA record's TTL. Create new names days ahead.

Serve-stale. RFC 8767 lets a resolver use expired data for a suggested 1 to 3 days when authoritative servers are unreachable. Do not change DNS provider in the same window.

Java. InetAddress has its own cache, controlled by networkaddress.cache.ttl. In Java 21 the default is forever with a security manager and implementation-specific without one, which is the usual case today. So set it explicitly, or time a test name change in rehearsal. Failed lookups use networkaddress.cache.negative.ttl, default 10 seconds. If in doubt, restart Java services after the change.

Existing connections. A connection pool that stays open keeps using the old address however low the TTL was, which is one reason to fence the source.

What to choose and why

Decide rollback by stage, before the night

The way back depends on where the newest committed data lives. Write this table for your own system.

Stage Where the newest data is Rollback Data cost
A. Source live, target only replicating Source Cancel None
B. Source fenced, target not promoted Source See the next table None
C. Target promoted, no client writes Source and target equal Fence target, then see the next table None
D. Clients writing, reverse replication healthy Target Planned failback at proven zero lag None, with a second outage
E. Clients writing, no reverse replication Target only Reconcile target writes or explicitly accept loss before any return All writes since promotion, unless exported and reconciled

Stages B and C are not the same action on every path. This table is our reading of the sources and has not been lab tested.

Path Stage B Stage C
PostgreSQL Remove fence, restart applications Remove fence on the old primary and restart applications. Rebuild the standby later
Always On Remove fence, restart applications, because no failover has happened The source is now a secondary, so removing a fence does not make it writable. Run another planned failover back, confirm online, then restart applications
Log shipping Before the tail-log backup, remove fence and restart applications. After it, the source is in RESTORING. A DBA must use the supported recovery procedure before reopening the source, accounting for the replication consequences The same source-recovery decision applies; exclude target writers first
Whole-VM Remove fence, restart the source Shut down or isolate the target, then restart the source

WITH RECOVERY has a cost. Once recovered, the source can no longer take log backups from the new primary, so replication must be seeded again before another attempt.

Stage E is the point of no return in practice. If losing those writes is unacceptable, your plan has to end the rollback option before clients start writing, or build the reverse path first.

Reverse paths differ by technology.

  • Always On: after failover, the former primary becomes a secondary and synchronises from the new primary. Keep it connected and failback is another planned failover.
  • Log shipping: the source left in RESTORING by the tail-log backup can receive log backups from the new primary. Set that job up before opening to users.
  • PostgreSQL: pg_rewind can bring the old primary back as a standby of the new one. It needs a cleanly shut down target, full_page_writes on, either wal_log_hints or data checksums, and WAL reaching back to the point of divergence. PostgreSQL warns that if it fails while processing, the target's data folder is "likely not in a state that can be recovered", so back up the old primary first or be ready to rebuild it. Configuration files are copied from the source, so correct the recovery settings before restarting.
  • MGN and Azure Migrate: the pages reviewed describe no replication from target back to source. MGN's Revert to "ready for cutover" changes the lifecycle status only, and after finalising MGN stops replication. Treat VM-level rollback as stage E once clients write, unless the database has its own reverse path.

Validation gates

A gate is a named person, a measurement, a pass value and an abort action. Appoint one person to decide. If a gate has no result by its deadline, stop progression and invoke the pre-agreed recovery decision. Do not automatically restart an older source after target writes have begun.

Gate Owner Evidence Pass If it fails
0. Readiness Migration lead Rehearsal timings, test migration, rollback prerequisites, TTL on every authoritative server All recorded Postpone
1. Zero lag DBA Pass condition for the path, marker row on target Position reached, marker present Stage B, or C where the check follows takeover
2. Target data DBA Database online, row counts on key tables, logins and jobs present Match the source record Stage C rollback
3. Private test Application owner Login and a controlled transaction using a private path Application result and all side effects accounted for Reassess writer state; a test write ends the no-write assumption
4. Client move Network lead Connections arriving at target, none at the fenced source Traffic as expected Stage D or E once target writes begin
5. Close Business owner Error rate, batch job and integration checks Accepted Stage D rollback, or E by explicit decision

A marker is a useful identity and path check, not a complete consistency proof: after writers stop, insert one uniquely identified row on the source, then read it on the target. It catches replication of the wrong database or schema, which a healthy lag figure cannot.

Where gate 1 runs depends on whether the target can be read:

  • PostgreSQL hot standby, or a readable Always On secondary: read the marker before takeover. A failure is stage B. Readability is configured per replica, so confirm it.
  • Non-readable Always On secondary: confirm the position from the primary before failover, and read the marker after it. A marker failure is stage C.
  • Log shipping: insert the marker before the tail-log backup, because that backup is what carries it, and read it after recovery. Stage C.
  • Whole-VM: the target does not exist until launch. Insert the marker before the source is shut down, and read it after launch. Stage C.

The runbook

Times are illustrative. Replace them with your rehearsal measurements.

Time Step Gate
T-14 days Full rehearsal: test migration, promotion, smoke test, rollback. Record every duration
T-3 days Lower TTL. Confirm on each authoritative server. Create any new names
T-1 day Freeze other changes. Confirm reverse-path prerequisites and backups 0
T+0:00 Stop applications, batch jobs and integrations
T+0:10 Fence clients from the source, keeping replication open. End existing sessions and confirm none remain
T+0:15 Insert marker. Record source position
T+0:20 Confirm position, and the marker where the target is readable 1
T+0:30 Promote, fail over or launch. Confirm databases online. Read the marker if not yet read 1
T+0:45 Compare data, logins and jobs 2
T+1:00 Private application test 3
T+1:30 Start reverse replication where supported, then change DNS
T+1:45 Watch arrivals at target. Restart Java or pooled services as planned 4
T+3:00 Business acceptance. Decide to close or roll back 5
After close Raise TTL. Retain the source in the planned isolated or reverse-replica state

The fence comes before promotion, so that no client with a stale answer can write to the old source after the target has taken over. On rollback, reverse the order: fence the target first, confirm it, then release the source.

The fence must not cut replication. A PostgreSQL standby connects with primary_conninfo, normally on the client port, so stopping the listener stops replication. Restrict by client address. Always On replicates through a separate database mirroring endpoint, which must stay open. A blocked port may leave established sessions running. The DBA's freeze procedure must account for them without interrupting replication or active transactions unexpectedly.

A DNS rollback obeys the same arithmetic, so keep the TTL low until gate 5 has closed.

Risks and limits

  • Fencing failure. If a client can write to both systems, you have two divergent databases. We know of no general way to merge them automatically.
  • Non-database state. Files, message queues, caches and scheduled jobs on the source are not covered by database lag. Add each to gate 2.
  • Limits of this article. PostgreSQL statements come from the version 18 documentation. MySQL and other engines are not covered.

Do not delete the source or finalise in the migration tool until the close gate has passed, and do not begin a cutover whose rollback has never been rehearsed.

Sources

Opened 2026-11-10.