Design a low-downtime PostgreSQL migration runbook using logical replication, measurable catch-up, write quiescence, sequence reconciliation, validation, rollback criteria, and slot cleanup.

Upgrade/Migration, CDC, Zero-Downtime Cutovers, and Logical Replication Runbooks

Design a low-downtime PostgreSQL migration runbook using logical replication, measurable catch-up, write quiescence, sequence reconciliation, validation, rollback criteria, and slot cleanup.

Intermediate → Advanced180–240 minutesLogical replication and data-movement labCurrent patched PostgreSQL 18.xCore PostgreSQL only for mandatory workDisposable local publisher/subscriber on ports 55439–55440Optional third node on port 55441 only where explicitly statedReplication, database CREATE, and table ownership privileges where statedLast reviewed: August 2026

Learning outcomes

Logical replication is often selected for low-downtime migration because source and destination can run different PostgreSQL major versions and can remain online while most data is copied. That capability does not make the cutover automatic. The hard part is establishing one writer, proving catch-up, validating business state, reconciling non-replicated state, and deciding when rollback is still safe.

01

Design a restore-first, validation-driven low-downtime migration runbook.

02

Separate initial copy, catch-up, quiescence, routing, and cleanup phases.

03

Prevent dual-write split state during cutover.

04

Reconcile sequences and unsupported objects before enabling target writes.

05

Distinguish built-in logical replication from external CDC platforms and transformation pipelines.

1. Define the migration contract before creating a subscription

Start with measurable acceptance criteria rather than commands. Define source and destination versions, published databases/tables, excluded objects, write ownership, allowable replication lag, maximum write freeze, validation queries, sequence/large-object handling, rollback point, and who owns slot cleanup.

Question Example ServiceHub decision
Source of truth before cutover Publisher on port 55439
Destination Subscriber on port 55440
Write freeze target Application rejects/queues writes during final drain
Data acceptance Key counts + invariant hashes + critical-row checks
Unsupported state Sequences, roles, DDL, large objects, extensions validated separately
Rollback boundary Before destination accepts independent writes, or explicit reverse-change plan exists

2. Initial synchronization and catch-up phase

Provision destination schema first, create publication/subscription, and allow initial synchronization to finish while the source application remains on the publisher. Track each table's pg_subscription_rel state plus apply-worker progress and source slot retention.

sql · subscriber: migration readiness snapshot
SELECT s.subname,       r.srrelid::regclass AS relation,       r.srsubstate,       r.srsublsnFROM pg_subscription_rel AS rJOIN pg_subscription AS s ON s.oid = r.srsubidWHERE s.subname = 'ch15_servicehub_sub'ORDER BY relation;SELECT subname, worker_type, received_lsn, latest_end_lsn,       last_msg_receipt_time, latest_end_timeFROM pg_stat_subscriptionWHERE subname = 'ch15_servicehub_sub';
sql · publisher: retained-WAL and consumer progress
SELECT slot_name, active, restart_lsn, confirmed_flush_lsn,       wal_status, safe_wal_size, inactive_since,       pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn) AS bytes_ahead_of_consumerFROM pg_replication_slotsWHERE slot_name = 'ch15_servicehub_sub';

One byte-distance sample is not a service-level objective. Observe the trend under real write load. A destination that is always 20 MB behind but catching up may be healthier than one that is 2 MB behind and continuously diverging.

3. Validate continuously, not only at the end

Run non-destructive validation while replication is active. Simple row counts are useful but not sufficient. Add domain invariants, aggregate totals, nullability/constraint checks, representative keys, and optionally deterministic chunked hashes where data volume permits.

sql · run the same business checks on source and destination
SELECT count(*) AS rows,       count(*) FILTER (WHERE status = 'completed') AS completed_rows,       sum(labor_minutes) AS total_labor_minutes,       min(work_order_id) AS min_id,       max(work_order_id) AS max_idFROM app.ch15_work_orders;SELECT region, status, count(*) AS rowsFROM app.ch15_work_ordersGROUP BY region, statusORDER BY region, status;

Differences can be expected while writes continue, so pair validation with a captured source progress point. Final equality is established only after write quiescence and complete subscriber catch-up.

4. Cutover: establish exactly one writer

The most dangerous “zero-downtime” mistake is running two independent writers against the same logical key space with no conflict-resolution design. Built-in logical replication is not an automatic multi-primary consensus system.

  1. Announce/enter a controlled write-quiescence window.
  2. Stop or queue source writes at the application/router layer.
  3. Capture the final source WAL position and a business marker.
  4. Wait until subscriber progress reaches the corresponding source change set.
  5. Run final data and schema validation.
  6. Reconcile sequences and all out-of-band state.
  7. Switch application connection/routing to the destination.
  8. Perform one controlled destination write and verify it.
sql · publisher: final marker during quiescence
BEGIN;INSERT INTO app.ch15_work_orders(work_order_id, customer_id, region, status, labor_minutes, changed_at)VALUES(15999, 5999, 'cutover', 'cutover_marker', 0, clock_timestamp());COMMIT;SELECT pg_current_wal_lsn() AS source_after_marker;
sql · subscriber: final marker and worker evidence
SELECT *FROM app.ch15_work_ordersWHERE work_order_id = 15999;SELECT subname, received_lsn, latest_end_lsn, latest_end_timeFROM pg_stat_subscriptionWHERE subname = 'ch15_servicehub_sub'  AND worker_type = 'apply';

5. Reconcile sequence and unsupported state before destination writes

Before routing writers, apply the Chapter 15 gap checklist: sequences/identities, large objects, roles and grants, extensions, generated-column compatibility, application jobs, certificates/secrets, server settings, and any schema changes performed outside the replication stream.

sql · example sequence acceptance query
-- For every sequence-backed key, verify sequence state against replicated data.SELECT max(work_order_id) FROM app.ch15_work_orders;-- Then set the actual destination sequence only if this table uses one,-- using the verified sequence name and after source writes are frozen.

Do not blindly run setval(max(id)) for every sequence. Some sequences intentionally allocate outside current table maxima; others feed multiple tables. Reconciliation must understand the application's allocation contract.

6. Rollback is easy only before destination divergence

Before the destination accepts independent writes, rollback can often mean “route back to the source and resume.” After destination writes begin, the two sides can diverge. A rollback then requires a designed reverse replication path, captured change set, or another reconciliation strategy. Do not claim a reversible migration if the runbook has not defined this state transition.

Cutover invariant

At every moment, identify exactly one authoritative writer for each replicated key space. If you cannot prove that invariant, the migration is in a dual-write risk state.

7. Cleanup only after the rollback window closes

A finished migration can leave logical slots retaining WAL forever if nobody owns teardown. After the destination is accepted, the old source is no longer needed for rollback, and all audit artifacts are preserved, remove the subscription/slot through normal PostgreSQL commands.

sql · subscriber: disable and remove the subscription after acceptance
ALTER SUBSCRIPTION ch15_servicehub_sub DISABLE;-- Confirm rollback window is closed and slot cleanup is approved.DROP SUBSCRIPTION ch15_servicehub_sub;
sql · publisher: verify no abandoned logical slots remain
SELECT slot_name, slot_type, active, restart_lsn,       confirmed_flush_lsn, wal_status, inactive_sinceFROM pg_replication_slotsWHERE slot_type = 'logical';

If a network failure prevents subscription drop from removing a remote slot, follow documented slot-disassociation/drop procedures. Do not delete files from pg_wal or the replication-slot directory manually.

8. Built-in replication versus external CDC

Change Data Capture (CDC) is a broader integration category. External systems may use logical decoding plugins, Kafka-compatible platforms, connectors, schema registries, transformation layers, or non-PostgreSQL sinks. Those tools introduce their own delivery guarantees, checkpoint formats, schemas, ordering rules, and failure modes.

Built-in PostgreSQL logical replication is strongest when the destination is PostgreSQL and same-named target tables can receive transactional table changes. It is not a general event bus, schema-transform engine, long-term audit log, or automatic bidirectional conflict resolver.

9. PostgreSQL major-version upgrade notes

Logical replication supports migration between different PostgreSQL major versions, making it useful for low-downtime upgrades. Cross-version migration still requires extension/type/DDL compatibility testing. PostgreSQL 18 also documents special procedures for upgrading clusters that already participate in logical replication; logical-slot migration through pg_upgrade requires old logical-replication cluster members to be PostgreSQL 17 or later.

Do not conflate “using logical replication to migrate application data to a new major version” with “upgrading an existing logical-replication topology in place.” They are related but operationally different projects.

10. Final ServiceHub migration drill

Recreate the Chapter 15 publisher/subscriber pair, perform initial synchronization, create ordinary source writes while observing catch-up, run validation, enter a write freeze, write the cutover marker, wait for the destination, reconcile non-replicated state, route one test writer to the destination, and record measured timings. The drill is successful only if the data contract and single-writer invariant are proven—not merely because CREATE SUBSCRIPTION succeeded.

Check your understanding

  1. What must be true before final cutover validation can establish source/destination equality?
  2. Why is dual-writing both databases unsafe by default?
  3. Why should sequence reconciliation occur after source write quiescence?
  4. When does rollback become materially harder?
  5. What is the boundary between built-in logical replication and a general CDC platform?
Review the answers

Final equality needs source writes frozen and the subscriber caught up to the final source change set. Independent dual writers can create conflicting states because built-in logical replication is not consensus/multi-primary conflict resolution. Sequence reconciliation done while source writes continue can immediately become stale. Rollback becomes harder once destination-only writes exist. Built-in logical replication moves selected PostgreSQL table changes; a general CDC platform may add transformations, external sinks, event infrastructure, and different delivery semantics.

Authoritative references

Logical replication is version-, privilege-, topology-, and schema-sensitive. These PostgreSQL 18 primary sources define the behavior used in this lesson.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.