Replication and WAL9 min read

MySQL GTIDs vs PostgreSQL Replication Slots: Tracking Position

GTIDs make MySQL failover repointing almost trivial; replication slots make PostgreSQL WAL retention automatic but risky. A practitioner's comparison of the two position-tracking models.

Every replication system has to answer the same unglamorous question: how does a replica tell the primary where it left off? MySQL and PostgreSQL answer it with mechanisms so different that engineers moving between them regularly reach for the wrong mental model. GTIDs are coordinates that travel with the data. Replication slots are memory that lives on the primary. Confusing the two leads to real incidents, and one of them can fill a disk.

I have run GTID-based MySQL topologies and slot-based PostgreSQL clusters in production, and I want to lay out what each mechanism actually does, where each one shines, and what you must monitor on both sides. If you are a MySQL team heading toward PostgreSQL, the slot retention section is the part to read twice.

GTIDs: every transaction gets a global name

A MySQL global transaction identifier is a pair: the server_uuid of the server where the transaction was first committed, plus a monotonically increasing sequence number. A server's replication state is then just a set of GTIDs, exposed as gtid_executed, with compact range notation like a-uuid:1-4913772. Because the identifier travels inside the binlog stream, every server in the topology knows exactly which transactions it has applied, regardless of which file and offset they arrived through.

Before GTIDs, MySQL replicas tracked position as a binlog filename and byte offset on their specific source, which meant repointing a replica to a new source required careful, error-prone coordinate translation. With gtid_mode=ON and auto-positioning, the replica simply tells its new source what it has, and the source sends what is missing.

-- On the replica: repoint to a new source with auto-positioning
CHANGE REPLICATION SOURCE TO
  SOURCE_HOST = 'new-primary.internal',
  SOURCE_AUTO_POSITION = 1;
START REPLICA;

-- Compare state across servers
SELECT @@global.gtid_executed;
SELECT @@global.gtid_purged;

The catch is gtid_purged. GTIDs identify transactions, but the transactions themselves still live in binlog files, and binlog files expire on a timer (binlog_expire_logs_seconds, default 30 days, often tuned far lower to save disk). If a replica is offline long enough that the source has purged binlogs containing GTIDs the replica never received, replication fails and the replica must be rebuilt or reseeded from backup. MySQL, by default, will not retain binlogs just because some replica might come back. The burden of retention is a timer, not a promise.

Replication slots: the primary remembers for you

PostgreSQL inverts that arrangement. A replication slot is a named, durable object on the primary whose entire job is to make a promise: WAL that this consumer still needs will not be removed. Physical slots serve streaming standbys; logical slots serve logical decoding consumers such as CDC pipelines. As a standby confirms progress, the slot's restart_lsn advances and older WAL becomes reclaimable.

Position tracking is therefore not a global transaction set but a log sequence number, an address in a single ordered WAL stream. There is no concept of merging transaction sets from multiple sources, because physical replication has no multi-source topologies to merge. It is a simpler model attached to a simpler topology, and within that scope it is airtight: a standby with a slot can be down for an hour or a weekend and pick up exactly where it stopped.

The promise is also the hazard. A slot whose consumer never comes back keeps holding WAL forever, and the primary will retain those files until the disk fills, at which point PostgreSQL stops accepting writes. This is not a rare exotic failure. It is probably the single most common self-inflicted PostgreSQL outage among teams that adopt CDC tooling, create a slot for an experiment, and forget it. Since PostgreSQL 13, max_slot_wal_keep_size caps the damage by invalidating slots that exceed a WAL retention budget, and the pg_replication_slots view grew wal_status and safe_wal_size columns so you can see how close each slot is to the edge. Invalidation saves the primary but sacrifices the consumer, which must then resynchronize from scratch, so the cap is a circuit breaker, not a substitute for monitoring.

Failover repointing: where GTIDs genuinely shine

Credit where due: GTID failover ergonomics are excellent. When a MySQL primary dies, surviving replicas may each have slightly different transaction sets. GTIDs make the reconciliation explicit and mechanical: promote the most advanced replica, point the others at it with auto-positioning, and each one fetches exactly the transactions it lacks. Errant transactions, ones committed directly on a replica that the rest of the topology never saw, are detectable by comparing GTID sets before promotion. Orchestration tools automate all of this precisely because the coordinate system makes it automatable.

PostgreSQL failover is more constrained. Standbys track a timeline, and promotion forks a new timeline; other standbys can follow the new primary if their WAL position is compatible, sometimes needing pg_rewind if they had advanced past the divergence point. What does not travel gracefully through failover, historically, is the slots themselves: slots are local to the server they were created on, so a slot on the failed primary simply does not exist on the promoted standby. For physical standbys managed by an HA framework this is handled by recreating slots. For logical consumers it was long the sore spot: after failover, your CDC pipeline's slot was gone. PostgreSQL 17 finally addressed this with failover slots, where logical slots marked with failover support are synchronized to a standby (sync_replication_slots), so a promoted standby can keep serving the same logical consumers. If CDC continuity through failover matters to you, that one feature is a strong reason to land on 17 rather than 16.

The monitoring story on the MySQL side

For GTID replication, the durable checks are: replication threads running, the gap between Retrieved_Gtid_Set and Executed_Gtid_Set on each replica, end-to-end lag via a heartbeat table rather than Seconds_Behind_Source alone, binlog disk consumption on the source, and a periodic errant-transaction check comparing replica GTID sets against the primary. None of this is difficult, but it is several separate signals, and the failure mode of missing one is a replica you cannot repoint when you need it most.

The monitoring story on the PostgreSQL side

On PostgreSQL, slot monitoring is one view and some LSN arithmetic, and it is non-negotiable for any cluster with slots.

SELECT slot_name,
       slot_type,
       active,
       wal_status,
       safe_wal_size,
       pg_size_pretty(
         pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
       ) AS retained_wal
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;

Alert on two conditions: any slot inactive longer than a few minutes when its consumer is supposed to be continuous, and retained WAL crossing a byte threshold sized to your disk headroom, well before max_slot_wal_keep_size would invalidate anything. Pair that with pg_stat_replication for lag on live standbys and with disk-space monitoring on the WAL volume, and the classic slot incident becomes a calm ticket instead of a write outage. The PostgreSQL monitoring guide covers where these signals fit in a broader baseline, and if you are running your own hardware, self-hosted PostgreSQL monitoring adds the disk-level context that managed services hide.

Which model do I prefer?

For failover choreography across a sprawling replica topology, GTIDs are the nicer abstraction, and I will not pretend otherwise. For a consumer that must never lose its place, slots are the stronger guarantee: MySQL's binlog expiry can strand a replica silently, while a PostgreSQL slot holds the data at a visible, measurable, alertable cost. My honest summary is that GTIDs optimize for topology flexibility and slots optimize for consumer safety, and each ecosystem's monitoring culture follows its risk: MySQL teams watch for replicas that fell off the log, PostgreSQL teams watch for slots that ate the disk. Logical replication setups inherit the slot model too, which the logical replication guide walks through in detail.

How MonPG helps once you are on PostgreSQL

MonPG is PostgreSQL-only, and slots are one of the reasons it exists. It tracks every replication slot's retained WAL continuously, flags inactive slots before retention becomes a disk event, and shows wal_status and safe_wal_size trends rather than point-in-time snapshots, so a slot drifting toward invalidation is visible days early. Alongside that it watches per-standby replication lag and WAL generation rate, which together answer the question slot graphs alone cannot: is the consumer slow, or is the primary suddenly loud? For a team arriving from MySQL, that is the GTID-era diligence translated into slot-era terms. See PostgreSQL monitoring for how the replication baseline fits together.