MySQL replication duplicates writes from a primary to one or more replicas using the binary log.
GTID mode automatically tracks transaction IDs, making failover simple and safe.
Position-based replication requires manual binlog file and offset tracking.
ROW-based logging is mandatory for consistency — STATEMENT mode causes silent data drift with functions like UUID().
Seconds_Behind_Source is unreliable — production monitoring needs heartbeat tables and GTID gap checks.
Parallel apply workers cut lag under heavy write loads, but must use LOGICAL_CLOCK parallelism.
✦ Definition~90s read
What is MySQL Replication Setup?
MySQL replication is the mechanism for copying data from one database server (the source, historically called 'primary') to one or more replicas (historically 'slaves'). It's the backbone of read scaling, high availability, and disaster recovery for countless production MySQL deployments.
★
Imagine a chef in a busy restaurant writing every order on a notepad as it comes in.
At its core, replication works by the source writing every data-changing statement or row event to a binary log (binlog), and each replica pulling those events from the source's binlog and replaying them locally. The two dominant modes are statement-based (SBR) and row-based (RBR); in practice, RBR is the default since MySQL 5.7 because it's safer and deterministic.
Global Transaction Identifiers (GTIDs) simplify tracking which transactions have been applied, making failover and recovery far less error-prone than the old file-position-based approach.
Semi-synchronous replication (semi-sync) was introduced to address a critical gap in async replication: if the source crashes before a replica has received a transaction, that transaction can be lost on failover. Semi-sync requires at least one replica to acknowledge receipt of a transaction (writing it to its relay log) before the source commits and returns success to the client.
This reduces the window for data loss to zero in theory, but in practice the timeout behavior matters enormously. When the source doesn't get an ack within rpl_semi_sync_source_timeout (default 10 seconds), it falls back to asynchronous mode — and during that fallback window, transactions can be committed on the source without any replica having seen them.
That's exactly how you lose 50 transactions in a crash: the timeout expired, the source went async, committed a batch, then died before any replica caught up.
Where does replication fit in the ecosystem? It's not a substitute for a distributed consensus system like Galera (Percona XtraDB Cluster, MariaDB Galera Cluster) or Group Replication (InnoDB Cluster). Those provide true multi-primary synchronous writes with automatic conflict detection, at the cost of higher latency and stricter network requirements.
Standard MySQL replication is simpler, lower overhead, and works across longer distances, but it gives you only eventual consistency and requires manual or tool-assisted failover (Orchestrator, ProxySQL, or MHA). You should not use standard replication when you need strong consistency guarantees, automatic failover with no data loss, or multi-region active-active writes.
For read scaling and basic HA with acceptable RPO (seconds to minutes), it's battle-tested and runs at massive scale — think millions of queries per second at companies like Uber, GitHub, and Booking.com.
Plain-English First
Imagine a chef in a busy restaurant writing every order on a notepad as it comes in. Now imagine a second chef in the back room who reads that same notepad in real time and prepares identical dishes. If the first chef gets sick, the second chef can step in immediately and nobody notices. MySQL replication works exactly like that — every write on your primary database is recorded in a log, and one or more replica servers read that log and replay the same changes. You get redundancy, read scaling, and a safety net, all from one elegant mechanism.
Every production database running at scale eventually hits the same wall: a single server can only handle so many reads before query times creep up, and a single point of failure is a liability no on-call engineer wants at 2 AM. MySQL replication is the foundational answer to both problems — it lets you distribute reads across multiple servers and gives you a warm standby that can take over within seconds if the primary crashes. It's not optional infrastructure. At any company running MySQL seriously, replication is day one.
The core problem replication solves is deceptively simple: how do you keep two or more MySQL instances in sync without locking everything or doubling write traffic? The answer lives in the binary log — a sequential, append-only journal of every state-changing event on the primary. Replicas connect, stream that log, and apply events in order. But the devil is in the details — GTID vs file-position tracking, parallel apply workers, semi-synchronous acknowledgment, replication filters, and lag under heavy write load are all areas where teams get burned in production.
By the end of this article you'll be able to configure a production-grade primary/replica pair from scratch, understand the exact bytes flowing between them, switch between GTID and position-based replication with confidence, monitor and diagnose replication lag, and avoid the three most expensive mistakes engineers make when they first set this up. This is not a tutorial for spinning up a throwaway demo — it's the setup you'd be comfortable putting in front of real traffic.
What Semi-Sync Replication Actually Guarantees (and Doesn't)
MySQL semi-synchronous replication ensures that for each transaction, the master waits for at least one replica to acknowledge receipt before returning a commit OK to the client. Unlike fully synchronous replication, the master does not wait for the replica to apply the transaction — only that it has been written to the replica's relay log. This reduces the window of data loss compared to async replication, but it does not eliminate it.
The key mechanic is a configurable timeout: if the replica does not acknowledge within rpl_semi_sync_master_timeout (default 10 seconds), the master falls back to asynchronous mode for that transaction. During that fallback window, any subsequent commit is at risk. In production, a network blip or overloaded replica can silently flip the master to async, and if the master crashes before the timeout expires, those unacknowledged transactions are lost — up to 50 in a typical high-throughput setup.
Use semi-sync when you need stronger durability than async but cannot afford the latency hit of fully synchronous replication (e.g., Paxos/MySQL Group Replication). It is a pragmatic middle ground for OLTP systems where a few lost transactions are tolerable but bulk losses are not. The real value is in reducing the replication lag window from seconds to milliseconds under normal conditions — but only if you monitor the fallback count and set timeouts aggressively.
Semi-Sync Is Not Sync
Semi-sync guarantees at least one replica has the transaction in its relay log — but if the master crashes before the replica applies it, that transaction is still lost.
Production Insight
A team lost 47 transactions during a master failover because a network microburst caused semi-sync to time out for 3 seconds, silently falling back to async. The symptom: the new master had a lower GTID set than the old master, and the application saw phantom writes disappear. Rule of thumb: set rpl_semi_sync_master_timeout to no more than 100ms and alert on Rpl_semi_sync_master_yes_transactions vs Rpl_semi_sync_master_off_tx ratio exceeding 1%.
Key Takeaway
Semi-sync reduces but does not eliminate data loss — a timeout fallback to async is the primary risk.
Monitor Rpl_semi_sync_master_off_tx and alert on any non-zero value in production.
Never rely on semi-sync alone for zero-data-loss failover; pair it with a quorum-based consensus layer if durability is critical.
thecodeforge.io
MySQL Semi-Sync Replication Flow and Pitfalls
Mysql Replication Setup
How MySQL Replication Actually Works Under the Hood
Before touching a config file, you need a mental model of what's actually happening. MySQL replication is driven by three threads. On the primary, the binlog dump thread streams binary log events to any replica that connects. On the replica, the I/O thread maintains a persistent TCP connection to the primary, receives those events, and writes them into a local file called the relay log. Meanwhile, the SQL thread (or parallel apply workers in modern MySQL) reads the relay log and executes each event against the replica's local storage engine.
The binary log is not a simple text log — it's a structured binary format that records events at the statement level (STATEMENT mode), the row level (ROW mode), or a hybrid of both (MIXED mode). ROW-based replication is what you want in production. It records the actual before and after image of each changed row, which makes it deterministic — no ambiguity about what a non-deterministic function like UUID() or NOW() returned. STATEMENT mode can cause silent data divergence when queries contain functions whose output differs between servers.
GTID (Global Transaction Identifier) is a unique ID assigned to every committed transaction on the primary, formatted as source_uuid:transaction_sequence. GTIDs make failover and replica reconfiguration dramatically safer because a replica can tell any server 'here are the GTIDs I already have — send me everything else,' without needing to know a specific file name and byte offset. For any new setup in 2024, GTID mode is the right default.
check_replication_internals.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
-- ============================================================-- Run these on the PRIMARY to inspect binlog state-- ============================================================-- Show all binary log files and their sizes on diskSHOWBINARYLOGS;
-- Output tells you how many logs exist and how much disk they consume-- Inspect the live stream of binary log events (human-readable)-- Replace 'mysql-bin.000003' with your current binlog fileSHOWBINLOGEVENTSIN'mysql-bin.000003'LIMIT20;
-- Confirm GTID mode is enabled and see executed GTID setsSHOWVARIABLESLIKE 'gtid_mode'; -- Should return ONSHOWVARIABLESLIKE 'enforce_gtid_consistency'; -- Should return ONSHOWMASTERSTATUS\G
-- gtid_executed column shows every GTID this server has committed-- ============================================================-- Run these on the REPLICA to inspect the three replication threads-- ============================================================-- The single most important replication health commandSHOWREPLICASTATUS\G
-- Key fields to check:-- Replica_IO_Running: Yes -- I/O thread is connected to primary-- Replica_SQL_Running: Yes -- SQL thread is applying relay log-- Seconds_Behind_Source: 0 -- 0 means replica is caught up-- Last_IO_Error: (empty) -- Any value here means connection trouble-- Last_SQL_Error: (empty) -- Any value here means apply-side failure-- Retrieved_Gtid_Set -- GTIDs received from primary-- Executed_Gtid_Set -- GTIDs applied to this replica's data-- Check how many parallel SQL apply workers are configuredSHOWVARIABLESLIKE 'replica_parallel_workers'; -- 0 = single-threadedSHOWVARIABLESLIKE 'replica_parallel_type'; -- LOGICAL_CLOCK is the modern choice
Output
-- SHOW BINARY LOGS output (example):
+------------------+-----------+-----------+
| Log_name | File_size | Encrypted |
+------------------+-----------+-----------+
| mysql-bin.000001 | 177 | No |
| mysql-bin.000002 | 52428800 | No |
| mysql-bin.000003 | 1024312 | No |
+------------------+-----------+-----------+
-- SHOW MASTER STATUS\G output (example with GTID):
If your binlog_format is set to STATEMENT and any query calls UUID(), RAND(), NOW(), or a user-defined function, the replica will silently execute that function again and get a different result. You'll have data drift with zero error messages. Set binlog_format=ROW in production — always. The extra disk usage is worth the determinism guarantee.
Step-by-Step: Configuring a GTID-Based Primary/Replica Pair
Let's build a working replication setup. We'll use two servers: primary at 10.0.1.10 and replica at 10.0.1.11, both running MySQL 8.0+. The configuration changes require a restart, so plan your maintenance window accordingly — or use MySQL's SET PERSIST for variables that support online changes.
The primary needs the binary log enabled with a unique server ID, and it needs a dedicated replication user with minimal privileges. The replication user should only have REPLICATION SLAVE privilege — nothing else. Least privilege matters here because this account's credentials live in plaintext in older versions, and even with mysql.slave_master_info table encryption in 8.0, you don't want a compromised replica credential giving an attacker DML access.
On the replica side, you set a distinct server ID, point it at the primary using CHANGE REPLICATION SOURCE TO (the modern syntax replacing CHANGE MASTER TO), and start the replication threads. With GTID mode, you don't need to know a binlog file or position — you just tell the replica 'start from the beginning of GTIDs I don't have.' That's the magic of GTID: the protocol figures out the gap automatically.
For an initial data load, use mysqldump with --single-transaction --master-data=2 --gtids, or for large datasets, Percona XtraBackup which takes a hot physical copy without locking tables. Never skip the initial consistent snapshot — if you do, the replica starts with wrong data and GTID tracking gives you a false sense of correctness.
mysql_primary_replica_config.cnfINI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# ============================================================
# PRIMARYSERVER: /etc/mysql/mysql.conf.d/mysqld.cnf
# Apply to server at 10.0.1.10, then restart MySQL
# ============================================================
[mysqld]
# Every server in a replication topology MUST have a unique ID
server-id = 1
# Enable the binary log — replication cannot work without this
log-bin = mysql-bin
binlog_format = ROW # Deterministic row-level events
binlog_row_image = FULL # Log complete before+after row images
# Retain binlogs for7 days — long enough for replicas to catch up after downtime
binlog_expire_logs_seconds = 604800
# GTID settings — both must be ON together
gtid_mode = ON
enforce_gtid_consistency = ON
# Crash safety: flush binlog to disk on every commit (safer, slightly slower)
sync_binlog = 1
innodb_flush_log_at_trx_commit = 1
# ============================================================
# REPLICASERVER: /etc/mysql/mysql.conf.d/mysqld.cnf
# Apply to server at 10.0.1.11, then restart MySQL
# ============================================================
[mysqld]
server-id = 2 # Must differ from primary's ID
# Relay log stores events received from primary before they're applied
relay-log = relay-bin
relay_log_recovery = ON # Auto-recover relay log on crash
# Write replica's own changes to its binlog — essential ifthis replica
# might ever become a primary (chained or cascaded replication)
log-bin = mysql-bin
log_replica_updates = ON
# GTID — must match primary
gtid_mode = ON
enforce_gtid_consistency = ON
# Parallel apply workers dramatically reduce lag under heavy write workloads
# LOGICAL_CLOCK preserves transaction ordering from the primary's commit order
replica_parallel_workers = 4
replica_parallel_type = LOGICAL_CLOCK
# Make replica read-only — prevents accidental writes that cause divergence
read_only = ON
super_read_only = ON # Also blocks SUPER privilege users
Output
# After restarting MySQL on both servers, verify with:
# On PRIMARY:
mysql> SHOW VARIABLES LIKE 'server_id';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| server_id | 1 |
+---------------+-------+
mysql> SHOW VARIABLES LIKE 'gtid_mode';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_mode | ON |
+---------------+-------+
# On REPLICA:
mysql> SHOW VARIABLES LIKE 'server_id';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| server_id | 2 |
+---------------+-------+
mysql> SHOW VARIABLES LIKE 'read_only';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| read_only | ON |
+---------------+-------+
Pro Tip: super_read_only Is Your Safety Net
Setting only read_only=ON still allows users with the SUPER privilege to write to the replica — and in MySQL 8.0 almost every application user has been granted it carelessly at some point. Set super_read_only=ON on every replica. It blocks writes from ALL users including root, and is the only reliable guard against accidental replica writes that cause silent data divergence. You can disable it temporarily in an emergency with SET GLOBAL super_read_only=OFF.
Bootstrapping Replication, Monitoring Lag, and Handling Failover
Configuration files are just the foundation. Actually starting replication requires three steps: create the replication user on the primary, take a consistent snapshot of primary data and load it into the replica, then issue CHANGE REPLICATION SOURCE TO on the replica and START REPLICA.
Lag monitoring is where teams get caught off guard. Seconds_Behind_Source in SHOW REPLICA STATUS is calculated as the difference between the current clock time and the timestamp embedded in the relay log event being applied. This sounds fine until you realize it resets to NULL when the SQL thread stalls, reports 0 when the I/O thread is lagging (the replica thinks it's caught up because it hasn't received the new events yet), and can spike from 0 to thousands of seconds instantly during a large transaction. Production monitoring should combine Seconds_Behind_Source with the GTID gap and heartbeat tables.
For failover, the clean approach with GTIDs is to run STOP REPLICA on the old replica, verify Executed_Gtid_Set matches the primary's gtid_executed, then promote with STOP REPLICA; RESET REPLICA ALL; SET GLOBAL read_only=OFF. Tools like Orchestrator or MySQL Router automate this entire flow and handle edge cases like errant transactions that exist on a replica but not the primary — a situation that can permanently break GTID-based replication if you try to re-add that server.
bootstrap_and_monitor_replication.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
-- ============================================================-- STEP 1: Run on PRIMARY — create the replication user-- ============================================================CREATEUSER'replication_user'@'10.0.1.11'IDENTIFIEDWITH caching_sha2_password -- Modern auth plugin for MySQL 8.0BY'StrongR3plPass!2024';
-- Only grant the minimum required privilegeGRANTREPLICATIONSLAVEON *.* TO'replication_user'@'10.0.1.11';
FLUSHPRIVILEGES;
-- Verify the user exists with the right privilegesSHOWGRANTSFOR'replication_user'@'10.0.1.11';
-- ============================================================-- STEP 2: Take a consistent snapshot on PRIMARY-- Run this in a shell, NOT in mysql client-- --single-transaction avoids table locks for InnoDB-- --source-data=2 adds CHANGE REPLICATION SOURCE comment to dump-- --triggers --routines --events captures full schema-- ============================================================-- $ mysqldump \-- --single-transaction \-- --source-data=2 \-- --set-gtid-purged=ON \-- --triggers \-- --routines \-- --events \-- --all-databases \-- -u root -p > /backup/primary_snapshot_$(date +%Y%m%d_%H%M%S).sql-- ============================================================-- STEP 3: Load snapshot on REPLICA (shell command)-- ============================================================-- $ mysql -u root -p < /backup/primary_snapshot_2024XXXX_XXXXXX.sql-- ============================================================-- STEP 4: Run on REPLICA — connect to primary and start replication-- GTID mode means we don't need SOURCE_LOG_FILE or SOURCE_LOG_POS-- ============================================================CHANGEREPLICATIONSOURCETO
SOURCE_HOST = '10.0.1.10',
SOURCE_PORT = 3306,
SOURCE_USER = 'replication_user',
SOURCE_PASSWORD = 'StrongR3plPass!2024',
SOURCE_AUTO_POSITION = 1, -- This is the GTID magic: find my own gap automatically
SOURCE_SSL = 1, -- Always use SSL for replication in production
SOURCE_SSL_CA = '/etc/mysql/ssl/ca.pem',
SOURCE_SSL_CERT = '/etc/mysql/ssl/replica-cert.pem',
SOURCE_SSL_KEY = '/etc/mysql/ssl/replica-key.pem',
SOURCE_CONNECT_RETRY = 10, -- Retry connection every 10 seconds on disconnect
SOURCE_RETRY_COUNT = 86400; -- Retry for up to 24 hours before giving up-- Start both replication threadsSTARTREPLICA;
-- Confirm everything is healthySHOWREPLICASTATUS\G
-- ============================================================-- ONGOING MONITORING: Detect real lag using a heartbeat approach-- Run on PRIMARY every 5 seconds (via cron or application job)-- ============================================================CREATETABLEIFNOTEXISTS monitoring.replication_heartbeat (
server_id INTNOTNULL,
heartbeat_ts DATETIME(6) NOTNULL,
PRIMARYKEY (server_id)
) ENGINE=InnoDB;
-- Insert/update the heartbeat timestampINSERTINTO monitoring.replication_heartbeat (server_id, heartbeat_ts)
VALUES (@@server_id, NOW(6))
ONDUPLICATEKEYUPDATE heartbeat_ts = NOW(6);
-- Run this on REPLICA to calculate true lag in secondsSELECTTIMESTAMPDIFF(
MICROSECOND,
heartbeat_ts,
NOW(6)
) / 1000000.0AS replication_lag_seconds,
heartbeat_ts AS last_primary_write
FROM monitoring.replication_heartbeat
WHERE server_id = 1; -- Primary's server_id-- ============================================================-- FAILOVER: Promote replica to primary (after primary failure)-- ============================================================-- Verify replica has all transactions before promotingSHOWREPLICASTATUS\G -- Confirm Seconds_Behind_Source: 0-- Stop replica threads cleanlySTOPREPLICA;
-- Disconnect from old primary and clear replication configRESETREPLICAALL;
-- Lift read-only restrictionsSETGLOBAL super_read_only = OFF;
SETGLOBAL read_only = OFF;
-- Verify this server is now writable and has all GTIDsSHOWMASTERSTATUS\G
SHOWVARIABLESLIKE'read_only';
Interviewers love this one. Seconds_Behind_Source measures the timestamp difference between now and the relay log event being applied — but if the I/O thread is lagging (primary is far ahead but events haven't been received yet), the metric shows 0 while you're actually minutes behind. Always pair it with GTID set comparison or a heartbeat table for trustworthy lag detection in production alerting.
Replication Lag in Production: Root Causes and Monitoring Beyond Seconds_Behind_Source
Replication lag is the silent killer of read scalability. Your application reads from replicas thinking it's getting fresh data, but if the replica is behind, every query returns stale results. The problem is that nobody notices until a customer complains about missing an order that was created 30 seconds ago.
The root causes of lag fall into three categories
Long-running transactions on primary: A single UPDATE that modifies a million rows generates a giant binlog event. The replica must apply that entire event in a single transaction, which can block the SQL thread for minutes.
Parallel replication misconfiguration: If you set replica_parallel_workers > 0 but leave replica_parallel_type = DATABASE, parallelism only works across different databases. A single-database app gets single-threaded apply. LOGICAL_CLOCK is the fix.
Hardware bottlenecks on replica: Replicas often run on cheaper hardware with slower disks. A write-heavy primary overwhelms the replica's I/O capacity, causing relay log buildup and lag.
Monitoring lag properly requires a three-metric approach: (1) Seconds_Behind_Source for quick glance, (2) GTID gap: compute the difference between primary's gtid_executed and replica's retrieved_gtid_set, (3) Heartbeat table: insert a timestamp on primary every 5 seconds and read it on replica to calculate true lag independent of clock skew.
check_lag.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# Simple lag check combining GTID and heartbeat
PRIMARY_GTID=$(mysql -h 10.0.1.10 -e "SELECT @@gtid_executed" -s 2>/dev/null)
REPLICA_GTID=$(mysql -h 10.0.1.11 -e "SELECT @@gtid_executed" -s 2>/dev/null)
MISSING=$(mysql -h 10.0.1.11 -e "SELECT GTID_SUBTRACT('$PRIMARY_GTID', '$REPLICA_GTID')" -s 2>/dev/null)
if [ "$MISSING" != "" ]; then
echo "WARNING: Replica missing $MISSING"else
echo "GTID sets are identical."
fi
HEARTBEAT_LAG=$(mysql -h 10.0.1.11 -e "\
SELECTTIMESTAMPDIFF(MICROSECOND, heartbeat_ts, NOW(6))/1000000.0 \
FROM monitoring.replication_heartbeat WHERE server_id=1" -s 2>/dev/null)
echo "Heartbeat lag: $HEARTBEAT_LAG seconds"
The Water Tank Mental Model for Replication Lag
Tap flow = write rate on primary (events per second).
Drain flow = apply rate on replica (constrained by hardware, parallelism, and transaction size).
Tank level = relay log backlog. If tap flows faster than drain, tank overflows → replica crashes or disk runs out.
Seconds_Behind_Source is like measuring the time since the last drop of water hit the drain — not how much water is in the tank.
The heart of the problem: you need a lag metric that measures how far the water level is from the top, not just the drop rate.
Production Insight
The single most common lag cause we see: replica_parallel_type = DATABASE when the app uses one schema.
LOGICAL_CLOCK is the only mode that parallelizes within a single database, using the primary's binary log group commit timestamps.
Rule: if your app is single-database, set replica_parallel_type=LOGICAL_CLOCK or accept single-threaded apply.
Key Takeaway
Don't trust Seconds_Behind_Source alone.
Pair it with GTID gap and heartbeat table.
The three-metric approach catches all lag types.
Replication Lag Diagnosis Decision Tree
IfSeconds_Behind_Source > 0 AND Relay log file growing
→
UseReplica apply speed < primary write speed. Check I/O, parallel workers, and long transactions.
IfSeconds_Behind_Source = 0 BUT application sees stale data
→
UseI/O thread is lagging — primary has new events not yet received. Check network latency or primary load.
IfSeconds_Behind_Source NULL
→
UseSQL thread stalled (error or stop). Check Last_SQL_Error.
IfHeartbeat lag > 5 seconds despite Seconds_Behind_Source = 0
→
UseClock skew between servers. Sync NTP on both.
Failover Strategies and Common Pitfalls
Promoting a replica to primary is the moment of truth. You need to guarantee that the promoted replica has all the data the old primary committed, and that there's no split-brain scenario where two servers accept writes.
With GTID, the procedure is: STOP REPLICA; RESET REPLICA ALL; SET GLOBAL read_only=OFF. That's it — the replica already has all transactions the old primary committed (accounting for potential loss with async replication). The risk comes from errant transactions: a write that accidentally happened on the replica (e.g., a stray admin query, a cron job, or a buggy migration). That errant GTID will exist on the replica's executed set but not on the primary's. When you promote the replica, that transaction is now part of the new primary's history, and if you ever try to re-add the old primary, the old primary will see a GTID it doesn't have and refuse to connect.
Tools like Orchestrator handle this by comparing gtid_purged and purging errant transactions with a controlled GTID skip. But the best defense is prevention: super_read_only=ON on all replicas, strict access controls, and never running any DML on replicas.
Another common pitfall: assuming semi-synchronous replication guarantees zero data loss. As we saw in the production incident, AFTER_SYNC mode (value at 1) allows the primary to commit before receiving the replica's ACK if the timeout expires. For zero loss, use AFTER_SYNC with a very high timeout or synchronous replication using a plugin that blocks the commit until the ACK arrives.
failover_rollback.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
# Script to safely promote replica and handle possible errant transactions
# Usage: ./promote_replica.sh <replica_ip>
set -e
REPLICA_IP=${1:?"Usage: $0 <replica_ip>"}
# Step1: Stop replica and check errant GtIDSMISSING=$(mysql -h $REPLICA_IP -e "SELECT GTID_SUBTRACT(@@gtid_executed, @@gtid_purged)" -s 2>/dev/null)
if [ -n "$MISSING" ]; then
echo "WARNING: Errant transactions detected: $MISSING"
echo "Manual intervention required to skip before promotion."
exit 1
fi
# Step2: Stop replication and promote
mysql -h $REPLICA_IP -e "STOP REPLICA; RESET REPLICA ALL; SET GLOBAL super_read_only=OFF; SET GLOBAL read_only=OFF;"
echo "Replica promoted to primary. Update application connection string to $REPLICA_IP"
The Errant Transaction Trap
Errant transactions are the #1 cause of GTID replication failures after failover. A single stray INSERT on a replica creates a GTID that the old primary won't have, making it impossible to re-add the old primary using auto-position. The fix requires manually injecting an empty skip transaction — step that many engineers forget until they're in a firefight. Prevention: super_read_only=ON in every replica config, not just at runtime.
Production Insight
We once lost an entire day of replication recovery because a legacy cron job ran UPDATE on a replica.
The errant GTID blocked re-adding the old primary after a failover test.
Fix: always set super_read_only=ON and audit replica access.
Key Takeaway
Failover is not the hard part.
Re-adding the old primary and handling errant transactions is where teams break.
Prevent errant writes with super_read_only; script the skip procedure before you need it.
Failover Decision Tree: Which Method to Use
IfPlanned switchover with both servers accessible
→
UseControlled failover: set old primary read-only, verify replica caught up, promote replica. Zero data loss possible with after_sync semi-sync.
IfPrimary unreachable, async replication
→
UseEmergency failover: promote the most advanced replica. Expect potential data loss of transactions in flight. Check Relay_Master_Log_File and Exec_Master_Log_Pos for guess.
UseFair chance of zero loss if the ACKed transactions were committed. Promote the replica that acknowledged them. Still verify via GTID gap.
IfNeed to re-add old primary after failover
→
UseHandle errant transactions: compare GTID sets, skip mismatches via SET GTID_NEXT. Use SET GLOBAL gtid_purged to align if needed (advanced, dangerous).
Prerequisites: The Bare Minimum to Avoid a 3AM Pager
Skip this section if you enjoy debugging cryptic connection errors on a Friday night. You need two MySQL 8.0+ servers — one source, one replica — with network connectivity on port 3306. Both must have GTID enabled (gtid_mode=ON) and enforce_gtid_consistency=ON. If you're on MySQL 5.7, upgrade. Seriously. Semi-sync replication and crash-safe slaves are vastly more reliable there.
Your replica needs enough disk to hold the entire source dataset plus binary logs for catch-up after failures. Under-provision disk and you'll learn about relay log corruption the hard way. Use SSDs on both ends — replication lag skyrockets on spinning rust under write load.
Firewall rules: allow inbound TCP/3306 from the replica's IP. Test with telnet before you start configuring. I've seen engineers waste two hours debugging 'Access denied' when the real problem was iptables silently dropping packets. Don't be that person.
PreReplicationSanityCheck.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — database tutorial
-- Verify GTID is on, both servers replicasSHOWVARIABLESLIKE'gtid_mode';
-- Expected: ONSHOWVARIABLESLIKE'enforce_gtid_consistency';
-- Expected: ON-- Check binary logging is enabled on sourceSHOWVARIABLESLIKE'log_bin';
-- Expected: ON-- Confirm replica can connect (run from replica shell)-- mysql -h <source_ip> -u rep_user -p -e 'SELECT 1'
Output
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_mode | ON |
+---------------+-------+
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| enforce_gtid_consistency | ON |
+------------------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin | ON |
+---------------+-------+
Production Trap:
If you're using MySQL 8.0 in a cloud environment like AWS RDS or Aurora, you can't set global gtid_mode on the fly. Enable GTID at instance creation or you'll need a maintenance window to flip it — which means downtime. Plan ahead.
Key Takeaway
GTID must be ON and stable on both servers before replication starts. Validate network connectivity separately from MySQL auth.
Understanding Replication in MySQL: What Actually Happens on the Wire
Here's the mental model: the source writes every committed transaction to its binary log (binlog). The replica connects, opens a TCP stream, and pulls those log events sequentially. Each event is a row change, statement, or GTID assignment. The replica writes them to its relay log, then replays them against its own data files. That's it. No magic. No dual-phase commits unless you've enabled semi-sync.
GTID changes the game. Without GTID, the replica tracks positions by filename and offset — brittle as hell after a crash or failover. With GTID, each transaction gets a unique identifier like '550e8400-e29b-41d4-a716-446655440000:1'. The replica knows exactly which transactions it has applied. Miss a failover? The replica will auto-skip duplicates and pick up where it left off. That's not convenience; that's survival in production.
Semi-sync adds a single ACK from the replica before the source commits to its client. It halves throughput but prevents data loss in a single-server crash. Use it on anything that bills customers or stores orders. Async is fine for analytics replicas where losing a few rows is acceptable — but you'd better have monitoring on lag.
ShowReplicationStatus.sqlSQL
1
2
3
4
5
6
7
8
// io.thecodeforge — database tutorial
-- Source side: check binlog file and positionSHOWMASTERSTATUS\G
-- Replica side: show I/O and SQL thread statusSHOWREPLICASTATUS\G
When diagnosing lag, never trust Seconds_Behind_Source alone — it resets to 0 when the SQL thread catches up even if the I/O thread is still receiving events. Monitor Retrieved_Gtid_Set vs Executed_Gtid_Set for real insight.
Key Takeaway
GTID makes replication crash-safe and failover-friendly. Semi-sync trades throughput for durability; use it on transactional systems, skip it on analytics.
Common Errors and Troubleshooting: What Kills Your Replication in the Dead of Night
Error 1032 (Can't find record) is the classic. You applied a DELETE or UPDATE on the source that doesn't match the replica's data — usually because someone ran a direct write on the replica. Fix: set sql_slave_skip_counter=1 and restart the SQL thread if you're not using GTID. With GTID, you inject a blank transaction to skip it, or you re-seed the replica from a fresh backup. Don't make this a habit; find the rogue writer.
Error 1062 (Duplicate entry) means the replica already has the row. Same root cause: writes on the replica. In production, set super_read_only=1 on all replicas. It prevents writes from application connections but still allows replication thread to apply changes. Non-negotiable.
Error 2003 (Can't connect to MySQL server on source) — network or firewall. Check the source's bind-address. In MySQL 8.0, the default is 127.0.0.1, so remote connections fail silently. Change it to 0.0.0.0 if you're not in a cluster. Yes, that exposes the port — use security groups, not the application's naivety.
Relay log corruption: the replica's relay log gets truncated during a crash. Solution: stop the replica, reset it with RESET REPLICA, and re-establish from the source's position. With GTID, you can just CHANGE MASTER TO MASTER_AUTO_POSITION=1 and skip the position hassle entirely.
FixCommonReplicationErrors.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// io.thecodeforge — database tutorial
-- Stop replica before any surgerySTOPREPLICA;
-- Skip one error (non-GTID only)SETGLOBAL sql_slave_skip_counter = 1;
STARTREPLICA;
-- With GTID: inject a blank transaction to skipSET GTID_NEXT='550e8400-e29b-41d4-a716-446655440000:351';
BEGIN; COMMIT;
SET GTID_NEXT='AUTOMATIC';
STARTREPLICA;
-- Reset relay log (GTID safe)RESETREPLICA;
CHANGEMASTERTO
MASTER_HOST='source.internal.local',
MASTER_USER='rep_user',
MASTER_PASSWORD='P@ssw0rd!',
MASTER_AUTO_POSITION=1,
GET_MASTER_PUBLIC_KEY=1;
STARTREPLICA;
-- Prevent future rogue writesSETGLOBAL super_read_only = ON;
Output
Query OK, 0 rows affected (0.01 sec)
Query OK, 0 rows affected (0.01 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.03 sec)
Query OK, 0 rows affected (0.02 sec)
Query OK, 0 rows affected (0.02 sec)
Production Trap:
Error 1032 and 1062 are symptoms, not root causes. If you're skipping them regularly, you have a bug in your application writing to the replica, or your backup/restore process is corrupting data. Fix the cause, not the symptom.
Key Takeaway
super_read_only=1 on all replicas prevents silent data drift. For GTID setups, skip errors with injected transactions, not sql_slave_skip_counter. Never ignore repeated errors — they're a red flag.
If Your Source Doesn’t Have Any Existing Data to Migrate: Skip the Dump, Go Straight to Replica
Most replication guides assume you have terabytes of production data to clone. That's not your problem. You're spinning up a fresh replica against a source that has zero application data or only insignificant test rows. Dumping and restoring a 10 MB schema is wasted time. You can go from CHANGE REPLICATION SOURCE TO to a fully synced replica in under two minutes. The key is to configure the replica with SOURCE_AUTO_POSITION = 1 and start replication immediately. GTIDs handle the rest. No dump, no restore, no --master-data. Just set the GTID_PURGED on the replica to match the source’s executed set, then start the slave thread. You verify by checking SHOW REPLICA STATUS for seconds_behind_source. If it's 0 and the IO and SQL threads are both Yes, you're done. This works because replication begins from the current position, not from a snapshot point. The replica catches any transactions committed after the change. For empty sources, there are no transactions to miss. Production trap: never skip this for populated databases—you'll corrupt your replica. Fresh source only.
fresh_replica_bootstrap.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// io.thecodeforge — database tutorial
// Bootstrap a replica from an empty source using GTIDs-- On source: verify GTID mode is ONSHOWVARIABLESLIKE'gtid_mode';
-- Output: ON-- On source: get executed GTID setSHOWMASTERSTATUS\G
-- Output:-- File: mysql-bin.000042-- Position: 194-- Binlog_Do_DB:-- Binlog_Ignore_DB:-- Executed_Gtid_Set: 00000000-0000-0000-0000-000000000001:1-5-- On replica: set GTID_PURGED to match source-- Must have empty gtid_executed firstSET @@GLOBAL.gtid_purged = '00000000-0000-0000-0000-000000000001:1-5';
-- Configure replication channelCHANGEREPLICATIONSOURCETO
SOURCE_HOST = '10.0.1.42',
SOURCE_PORT = 3306,
SOURCE_USER = 'repl_user',
SOURCE_PASSWORD = 'strong_password_here',
SOURCE_AUTO_POSITION = 1;
-- Start replicationSTARTREPLICA;
-- ValidateSHOWREPLICASTATUS\G
Replica_IO_State: Waiting for source to send event
Source_Log_File: mysql-bin.000042
Read_Master_Log_Pos: 194
Relay_Log_File: relay-bin.000002
Relay_Log_Pos: 320
Relay_Source_Log_File: mysql-bin.000042
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 0
Production Trap:
Setting gtid_purged without first verifying gtid_executed is empty on the replica will silently fail. If the replica already has its own GTID history, you'll get a 1682 error. Always run RESET MASTER on the replica before bootstrap if it's a fresh node.
Key Takeaway
Empty source = no dump needed. Set gtid_purged, use auto_position, start replica. Validated in seconds.
Authentication Plugin Compatibility Issues: Why Your Replica Won't Connect and How to Fix It for Good
You configured CHANGE REPLICATION SOURCE TO with the right IP, port, user, and password. Still getting error 1045: Access denied. There's nothing wrong with your credentials. The problem is the authentication plugin. MySQL 8.0 defaults to caching_sha2_password for new users. Older replicas or connectors expect mysql_native_password. The source sends an RSA public key challenge during the handshake. If the replica's client library can't handle that plugin, the connection dies before any password exchange. Two fixes. One: recreate the replication user with the old plugin. That's CREATE USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'secret'. This is the quick hack and works with any replica version 5.6+. Two: configure the source to accept both plugins and force native for the replica user. That's ALTER USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'secret'. Don't change the default plugin server-wide unless you audit every connection. The production fix: upgrade your replicas to MySQL 8.0.19+ which handles the handshake natively. But when a 3AM failover wakes you up and the replica can't authenticate, the native_password override is your lifeline. Just remember to rotate that user's password post-recovery.
fix_auth_plugin.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// io.thecodeforge — database tutorial
// Fix authentication plugin mismatch for replication user
-- On source: check current authentication plugin for userSELECT user, host, plugin
FROM mysql.user
WHERE user = 'repl_user';
-- Output:-- user | host | plugin-- repl_user | % | caching_sha2_password-- Error log on replica shows:-- [ERROR] Replica I/O for channel '': error connecting to master 'repl_user'@'10.0.1.42' - retry-time: 60 retries: 1-- Authentication plugin 'caching_sha2_password' cannot be loaded: plugin not enabled-- Fix: change plugin to native_password (no data loss, immediate effect)ALTERUSER'repl_user'@'%'IDENTIFIEDWITH mysql_native_password BY'new_strong_password';
-- Flush privileges to apply immediatelyFLUSHPRIVILEGES;
-- On replica: update password if changedSTOPREPLICA;
CHANGEREPLICATIONSOURCETO
SOURCE_PASSWORD = 'new_strong_password';
STARTREPLICA;
-- Verify connection worksSHOWREPLICASTATUS\G
Proactively create replication users with mysql_native_password from day one. Your future on-call self will thank you. If a vendor replica is older than 8.0.19, this is mandatory. Script it in your IaC and never think about it again.
Key Takeaway
caching_sha2_password breaks old replicas on connection handshake. Force mysql_native_password for replication users, or upgrade all replicas to 8.0.19+.
● Production incidentPOST-MORTEMseverity: high
The 50 Lost Transactions: Semi-Sync Timeout Betrayed Us
Symptom
After promoting the replica to primary, the application reported 'Order not found' for 50 recently created orders that existed on the crashed primary's binary log but never reached the replica.
Assumption
Semi-synchronous replication was enabled, so every transaction acknowledged by the primary must have been received by at least one replica. The replica should have all committed data.
Root cause
Semi-sync was configured with rpl_semi_sync_source_timeout = 10000 (10 seconds). During a network hiccup, the primary timed out waiting for the replica's ACK, fell back to asynchronous replication, and committed 50 transactions that the replica never received. The primary log showed these transactions with after_sync flag false.
Fix
Switch to after_sync mode (rpl_semi_sync_source_wait_point = AFTER_SYNC) so the primary only commits after the replica ACKs. Set timeout to a value higher than the maximum observed replication latency, and alert on any fallback to async. Add synchronous mode (rpl_semi_sync_source_enabled = AFTER_SYNC) and require at least one replica ACK before commit.
Key lesson
Semi-sync with AFTER_SYNC (after commit) is misleading — it allows commits before replica ACK. Always use AFTER_SYNC (wait before commit).
Monitor rpl_semi_sync_source_status and rpl_semi_sync_source_clients to detect fallback.
Never assume semi-sync guarantees zero data loss without verifying the mode and timeout settings.
Production debug guideCommon replication problems and the exact commands to run5 entries
Symptom · 01
SHOW REPLICA STATUS shows Replica_IO_Running: No
→
Fix
Check Last_IO_Error for connection error. Verify network connectivity, primary is reachable, and replication user credentials are correct. Run mysql -u replication_user -p -h <primary_ip> to test basic connectivity.
Symptom · 02
SHOW REPLICA STATUS shows Replica_SQL_Running: No
→
Fix
Check Last_SQL_Error for apply error (e.g., duplicate key, row not found). Use SHOW BINLOG EVENTS or stop replica, skip the offending transaction with SET GTID_NEXT='<gtid>'; BEGIN; COMMIT; / or use sql_slave_skip_counter (only if not using GTID). Then START REPLICA.
Symptom · 03
Seconds_Behind_Source is growing continuously
→
Fix
Check if replica_parallel_workers > 0 and replica_parallel_type = LOGICAL_CLOCK. Run SHOW PROCESSLIST on replica to identify stuck SQL threads. Check disk I/O on replica using iostat. Verify the replication heartbeat table lag to get true lag.
Symptom · 04
Replica lag suddenly jumps to a very high value
→
Fix
Identify the large transaction that caused the jump. Use SHOW BINLOG EVENTS on primary or check MySQL slow query log for long-running transactions. Consider splitting large bulk operations into batches to reduce replication lag spike.
Symptom · 05
GTID replication fails with 'The slave is connecting using CHANGE MASTER TO MASTER_AUTO_POSITION = 1, but the master has purged binary logs containing GTIDs that the slave requires'
→
Fix
The primary purged binlogs containing GTIDs the replica hasn't applied. Use SET GLOBAL binlog_expire_logs_seconds to a longer retention. Rebuild the replica from a fresh consistent backup taken after the missing GTIDs were still available, or manually inject missing GTIDs using SET GTID_NEXT (if you know the missing GTIDs).
★ MySQL Replication Quick Debug Cheat SheetFirst-response commands for production replication issues
Replica lag > 10 seconds−
Immediate action
Run SHOW REPLICA STATUS to check Seconds_Behind_Source (SBS). Then check the GTID gap with SELECT MASTER_POS_WAIT('', -1) on replica and compare with primary.
Commands
SHOW REPLICA STATUS\G
SHOW PROCESSLIST; (on replica to see SQL/IO threads)
Fix now
If lag is due to a single large statement, consider setting replica_parallel_workers = 4 and replica_parallel_type = LOGICAL_CLOCK. Restart replica SQL thread: STOP REPLICA; START REPLICA;
IO thread not running+
Immediate action
Check Last_IO_Error in SHOW REPLICA STATUS. Verify network and credentials.
Identify the errant transaction GTID, skip it, then restart the SQL thread.
Commands
SHOW REPLICA STATUS\G | grep Last_SQL_Error
SHOW BINLOG EVENTS IN 'mysql-bin.00000X' LIMIT 10; (if using position) or SHOW GTID_SUBTRACT('retrieved', 'executed');
Fix now
SET GTID_NEXT='<errant_gtid>'; BEGIN; COMMIT; SET GTID_NEXT='AUTOMATIC'; START REPLICA;
GTID vs Position-Based Replication
Feature / Aspect
GTID-Based Replication
Position-Based (File+Offset) Replication
Failover complexity
Simple — CHANGE REPLICATION SOURCE with AUTO_POSITION=1
Complex — must find exact binlog file + byte position on new primary
Errant transaction risk
High visibility — GTID gap is detectable and blocks re-add
Silent — divergence can go undetected until queries return wrong data
Multi-source replication
Straightforward — GTIDs are globally unique across all sources
Error-prone — file/position namespaces can collide
Replication filter compatibility
Requires care — filtered transactions still consume a GTID
Filters work without GTID bookkeeping complications
Initial setup complexity
Slightly more config (enforce_gtid_consistency restrictions)
Simpler initial config
Tooling support (Orchestrator, ProxySQL)
First-class support — topology-aware failover built around GTIDs
Supported but requires more manual coordination
mysqldump compatibility
Requires --set-gtid-purged=ON flag
Works with default --master-data=2 flag
Crash recovery
Automatic GTID recovery — no manual position hunting
Manual: must check binlog position from InnoDB recovery
Production recommendation (2024)
✅ Default choice for all new setups
Legacy setups only — migrate to GTID when possible
Key takeaways
1
Always use ROW-based binary logging in production
STATEMENT mode causes silent data divergence with non-deterministic functions like UUID() and NOW(), with zero error output to alert you.
2
GTID mode with SOURCE_AUTO_POSITION=1 is the only replication setup worth building in 2024
failover drops from a manual 20-step process to 3 commands, and errant transactions become detectable instead of silent.
3
Seconds_Behind_Source is an unreliable lag metric on its own
pair it with a heartbeat table and GTID set comparison to get a lag number you can actually alert on in production.
4
super_read_only=ON must live in my.cnf, not just SET GLOBAL at runtime
a replica restart without it in the config file silently drops read protection, and the next errant write breaks your GTID topology at the worst possible moment.
5
Parallel apply workers with LOGICAL_CLOCK type are the difference between a replica that keeps up under load and one that falls behind into permanent lag.
Common mistakes to avoid
3 patterns
×
Duplicate server-id across the topology
Symptom
Replication connects but events are silently skipped or looped; SHOW REPLICA STATUS shows IO thread connecting and disconnecting in a loop with error 'Fatal error: The slave I/O thread stops because master and slave have equal MySQL server ids'.
Fix
Every server in the entire topology (primaries, replicas, and intermediate replicas) must have a globally unique server-id. Audit all servers with SELECT @@server_id before adding a new node, and document IDs in your infrastructure config management system.
×
Writing directly to a replica
Symptom
Data exists on the replica that never existed on the primary; GTID replication breaks with 'Error 1236: The slave is connecting using CHANGE MASTER TO MASTER_AUTO_POSITION = 1, but the master has purged binary logs containing GTIDs that the slave requires' on the next failover attempt.
Fix
Set both read_only=ON AND super_read_only=ON in the replica's my.cnf, not just at runtime. Runtime-only settings are lost on restart. Errant transactions that already exist must be skipped by injecting an empty transaction with the errant GTID: SET GTID_NEXT='errant-gtid:N'; BEGIN; COMMIT; SET GTID_NEXT='AUTOMATIC';
×
Setting replica_parallel_workers without LOGICAL_CLOCK
Symptom
Replica lag is high under write-heavy workloads even with parallel workers enabled; or worse, replica applies transactions out of order causing intermittent constraint violations and SQL thread errors.
Fix
If you enable replica_parallel_workers > 0, also set replica_parallel_type=LOGICAL_CLOCK and replica_preserve_commit_order=ON. LOGICAL_CLOCK uses the primary's binary log group commit timestamps to apply transactions in parallel only when it's safe to do so, preserving the original commit ordering. The default DATABASE parallelism mode only parallelizes across different schemas, which is nearly useless for single-database applications.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
A replica is showing Seconds_Behind_Source: 0 in SHOW REPLICA STATUS, bu...
Q02SENIOR
Explain the difference between a retrieved GTID set and an executed GTID...
Q03SENIOR
Your team needs to promote a replica to primary after an unexpected prim...
Q04SENIOR
What is the difference between asynchronous, semi-synchronous, and synch...
Q01 of 04SENIOR
A replica is showing Seconds_Behind_Source: 0 in SHOW REPLICA STATUS, but your application is reading stale data from it. How do you diagnose the true replication lag, and what could explain this discrepancy?
ANSWER
Seconds_Behind_Source measures the difference between the timestamp of the relay log event currently being applied and the current time on the replica. If the I/O thread is lagging (events not yet received), no events are being applied, so Seconds_Behind_Source may report 0 (since the last applied event is very recent relative to the replica's clock). To diagnose true lag: compare GTID sets between primary and replica using SELECT GTID_SUBTRACT(primary_gtid_executed, replica_gtid_executed). Also set up a heartbeat table on the primary that updates every second; read that value on the replica to see actual data freshness. Common causes: network latency, primary overloaded with writes, or large transactions that haven't been transmitted yet.
Q02 of 04SENIOR
Explain the difference between a retrieved GTID set and an executed GTID set on a replica, and describe a scenario where they would differ significantly. What does that difference tell you operationally?
ANSWER
Retrieved_Gtid_Set contains GTIDs that the I/O thread has received from the primary but not yet applied. Executed_Gtid_Set contains GTIDs that have been applied by the SQL thread. A significant difference (retrieved >> executed) indicates that the relay log backlog is growing — the replica is receiving events faster than it can apply them. This is a sign of apply lag. Operational action: check replica I/O, parallel worker config, or whether a large transaction is blocking the SQL thread.
Q03 of 04SENIOR
Your team needs to promote a replica to primary after an unexpected primary failure. You notice the replica's Executed_Gtid_Set is missing 50 transactions compared to the crashed primary's last known gtid_executed. The crashed primary is unrecoverable. How do you handle the missing transactions, and what are the tradeoffs of your decision?
ANSWER
In this scenario, the 50 missing transactions are permanently lost because the primary is unrecoverable. You have two options:
1) Promote the replica anyway and accept data loss: This is the fastest recovery but the application must handle the inconsistency (e.g., reconcile with logs, user reports). You'll lose the 50 transactions. This is the pragmatic choice for most systems where RPO is not zero.
2) Attempt to recover the transactions from the binary logs of the crashed primary if the disk is accessible: If the primary's disk is intact, you can extract the binlog files and apply the missing GTIDs to the replica manually. This adds downtime but can recover the data.
Tradeoff: RPO vs RTO. Option 1 gives RTO in minutes with some data loss; option 2 can achieve zero data loss but may take hours if the logs need to be transferred and applied. Also, after promoting, any attempt to re-add the crashed primary later will hit errant transaction issues because the old primary has GTIDs the new primary never had. Those must be handled via GTID skip.
Q04 of 04SENIOR
What is the difference between asynchronous, semi-synchronous, and synchronous replication in MySQL? Describe a production scenario where each is appropriate.
ANSWER
Asynchronous (default): Primary commits before sending binlog to replica. Fastest writes, but risk of data loss on primary failure. Appropriate for read replicas where some data loss is acceptable (e.g., reporting, caching). Semi-synchronous: Primary waits until at least one replica acknowledges receipt of the binlog event before committing. Reduces data loss window to almost zero if after_sync mode is used. Appropriate for high-value data like transaction logs. Synchronous (via Galera or Group Replication): Primary commits only when all replicas have committed. Zero data loss, but write latency increases to the slowest replica's commit time. Appropriate for financial or compliance workloads where consistency is paramount.
01
A replica is showing Seconds_Behind_Source: 0 in SHOW REPLICA STATUS, but your application is reading stale data from it. How do you diagnose the true replication lag, and what could explain this discrepancy?
SENIOR
02
Explain the difference between a retrieved GTID set and an executed GTID set on a replica, and describe a scenario where they would differ significantly. What does that difference tell you operationally?
SENIOR
03
Your team needs to promote a replica to primary after an unexpected primary failure. You notice the replica's Executed_Gtid_Set is missing 50 transactions compared to the crashed primary's last known gtid_executed. The crashed primary is unrecoverable. How do you handle the missing transactions, and what are the tradeoffs of your decision?
SENIOR
04
What is the difference between asynchronous, semi-synchronous, and synchronous replication in MySQL? Describe a production scenario where each is appropriate.
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
How do I add a replica to an existing MySQL primary without downtime?
Use Percona XtraBackup to take a hot physical backup of the primary — it streams a consistent snapshot without locking tables or interrupting writes. Restore the backup on the new replica server, note the GTID position embedded in the xtrabackup_binlog_info file, configure the replica's my.cnf with a unique server-id and GTID settings, then run CHANGE REPLICATION SOURCE TO with SOURCE_AUTO_POSITION=1 and START REPLICA. The replica will automatically request only the GTIDs it's missing.
Was this helpful?
02
What's the difference between MySQL semi-synchronous and asynchronous replication?
In standard asynchronous replication (the default), the primary commits a transaction and returns success to the client before confirming that any replica received the binlog events — so in a crash, you can lose committed data. Semi-synchronous replication (rpl_semi_sync_source_enabled=ON) makes the primary wait for at least one replica to acknowledge receipt of the binlog event before returning success to the client, guaranteeing zero data loss on primary failure. The tradeoff is added write latency equal to your network round-trip time to the nearest replica, typically 1-5ms in the same datacenter.
Was this helpful?
03
Can I replicate only specific tables or databases instead of everything?
Yes, using replication filters — set replicate-do-db, replicate-do-table, replicate-ignore-db, or replicate-ignore-table in the replica's my.cnf. However, these filters interact dangerously with GTID mode: even filtered-out transactions consume a GTID on the primary but aren't applied on the replica, creating a permanent GTID gap that causes errors if you ever try to use that replica as a new primary. The safer pattern for partial replication is to use logical replication tools like Debezium or to restructure your schema so independent data lives on separate MySQL instances entirely.
Was this helpful?
04
How do I recover from a binary log corruption on the primary?
If the binary log is corrupted and replication breaks, first determine the last good position: use mysqlbinlog with --start-position to read logs until error. If possible, skip the corrupt event using mysqlbinlog --skip-gtid-purge or stop replica on the replica, then start from a known good GTID position using CHANGE REPLICATION SOURCE TO with a file position. In severe cases, you may need to rebuild the replica from a new backup. The best prevention is to enable binary log checksums: SET GLOBAL binlog_checksum = CRC32; and monitor log integrity with CHECKSUM TABLE.
Was this helpful?
05
What is the impact of using a delayed replica (MASTER_DELAY)?
A delayed replica intentionally lags behind the primary by a configurable number of seconds (e.g., 1 hour). This is useful for recovery from accidental destructive operations (like DROP TABLE). The primary commits the change, and the replica waits the specified delay before applying it. You have that window to stop replication, set GTID_NEXT to skip the bad transaction, and restart without applying it. The tradeoff is that the replica is always behind, so it cannot be promoted in a failover unless you accept that delay.