MySQL Replication — Semi-Sync Timeout Lost 50 Transactions
Detect semi-sync fallback with SHOW REPLICA STATUS after timeout lost 50 orders.
20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.
- ✓Deep production experience
- ✓Understanding of internals and trade-offs
- ✓Experience debugging complex systems
- 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.
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.
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%.Rpl_semi_sync_master_off_tx and alert on any non-zero value in production.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.
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.
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.
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.
- 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.
- 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.
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.
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.
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.
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.
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.
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.
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.MySQL Group Replication vs InnoDB Cluster vs Async Replication
MySQL offers several replication technologies, each suited for different use cases. Understanding the differences between Group Replication, InnoDB Cluster, and traditional Async Replication is crucial for designing a robust high-availability architecture.
Async Replication is the classic master-slave setup where the source commits transactions without waiting for replicas. It's simple and low-latency but risks data loss on source failure. Semi-sync replication reduces this risk by requiring at least one replica to acknowledge receipt before the source commits.
Group Replication implements a multi-master update-anywhere replication model using a Paxos-based consensus protocol. It provides strong consistency, automatic failover, and built-in conflict detection. However, it requires a minimum of three nodes and has higher latency due to the consensus round.
InnoDB Cluster is a complete high-availability solution built on Group Replication, MySQL Shell, and MySQL Router. It automates configuration, failover, and client routing. InnoDB Cluster is ideal for environments requiring automatic failover and easy management, while raw Group Replication offers more flexibility for custom setups.
- Async/Semi-sync: Simple setups, low latency, manual failover acceptable.
- Group Replication: Multi-writer, strong consistency, automatic failover, but higher latency.
- InnoDB Cluster: Turnkey HA solution, automatic failover, easy scaling.
For production, InnoDB Cluster is recommended for most new deployments due to its ease of use and reliability. However, for existing async replication setups, migrating to Group Replication requires careful planning and testing.
MySQL Replication Lag: Monitoring and Mitigation
Replication lag is the delay between a transaction being committed on the source and applied on the replica. Monitoring only Seconds_Behind_Source is insufficient because it can be misleading—it measures only SQL thread lag, not I/O thread lag, and can reset to 0 if the replica catches up momentarily.
Better monitoring metrics: - Seconds_Behind_Source: Still useful as a baseline but unreliable during bursts. - Master_Log_File and Read_Master_Log_Pos vs Relay_Master_Log_File and Exec_Master_Log_Pos: Compare to detect I/O vs SQL lag. - gtid_executed and gtid_purged: For GTID-based setups, compare executed GTID sets. - threads_running and threads_connected: High concurrency on replica can indicate lag. - Performance Schema tables: replication_applier_status_by_worker for multi-threaded replicas.
Mitigation strategies: 1. Optimize replica hardware: Faster disks, more memory, and CPU can reduce apply time. 2. Use multi-threaded replication: Set slave_parallel_workers > 0 and slave_parallel_type='LOGICAL_CLOCK' for parallel apply. 3. Reduce source write load: Offload read queries to replicas, batch writes, or use sharding. 4. Monitor and tune: Use pt-heartbeat from Percona Toolkit for precise lag measurement. 5. Avoid long-running transactions: They block replication on the replica.
Example: Setting up multi-threaded replication: ``sql STOP SLAVE; SET GLOBAL slave_parallel_workers=4; SET GLOBAL slave_parallel_type='LOGICAL_CLOCK'; START SLAVE; ``
For critical systems, implement lag alerts based on GTID differences rather than Seconds_Behind_Source.
MySQL GTID-Based Replication: Setup and Failover
GTID (Global Transaction Identifier) simplifies replication management by uniquely identifying each transaction. With GTIDs, you don't need to track binary log file positions, making failover and setup easier.
Setup steps: 1. Enable GTID on both source and replica: gtid_mode=ON, enforce_gtid_consistency=ON. 2. On the source, create a replication user. 3. On the replica, configure CHANGE MASTER TO with MASTER_AUTO_POSITION=1. 4. Start replica.
Example configuration: ``ini [mysqld] gtid_mode=ON enforce_gtid_consistency=ON log_bin=mysql-bin binlog_format=ROW server-id=1 ``
Failover with GTIDs: When the source fails, promote a replica by: 1. Stop the replica: STOP SLAVE; 2. Reset slave: RESET SLAVE ALL; 3. Enable writes: SET GLOBAL read_only=OFF; 4. Point other replicas to the new source using MASTER_AUTO_POSITION=1.
GTIDs ensure that no transactions are lost or duplicated during failover. However, you must ensure all replicas have applied all transactions from the old source before promoting.
Common pitfalls: - Skipping transactions with SET GTID_NEXT can cause GTID gaps. - RESET MASTER on a source with replicas will break replication. - Mixing GTID and non-GTID replicas is not supported.
For automatic failover, consider using MySQL InnoDB Cluster or orchestration tools like Orchestrator.
The 50 Lost Transactions: Semi-Sync Timeout Betrayed Us
- 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.
SHOW REPLICA STATUS\GSHOW PROCESSLIST; (on replica to see SQL/IO threads)| File | Command / Code | Purpose |
|---|---|---|
| check_replication_internals.sql | SHOW BINARY LOGS; | How MySQL Replication Actually Works Under the Hood |
| mysql_primary_replica_config.cnf | [mysqld] | Step-by-Step |
| bootstrap_and_monitor_replication.sql | CREATE USER 'replication_user'@'10.0.1.11' | Bootstrapping Replication, Monitoring Lag, and Handling Fail |
| check_lag.sh | PRIMARY_GTID=$(mysql -h 10.0.1.10 -e "SELECT @@gtid_executed" -s 2>/dev/null) | Replication Lag in Production |
| failover_rollback.sh | set -e | Failover Strategies and Common Pitfalls |
| PreReplicationSanityCheck.sql | SHOW VARIABLES LIKE 'gtid_mode'; | Prerequisites |
| ShowReplicationStatus.sql | SHOW MASTER STATUS\G | Understanding Replication in MySQL |
| FixCommonReplicationErrors.sql | STOP REPLICA; | Common Errors and Troubleshooting |
| fresh_replica_bootstrap.sql | SHOW VARIABLES LIKE 'gtid_mode'; | If Your Source Doesn’t Have Any Existing Data to Migrate |
| fix_auth_plugin.sql | SELECT user, host, plugin | Authentication Plugin Compatibility Issues |
| group_replication_setup.sql | INSTALL PLUGIN group_replication SONAME 'group_replication.so'; | MySQL Group Replication vs InnoDB Cluster vs Async Replicati |
| monitor_lag.sql | SELECT GTID_SUBSET(@@GLOBAL.gtid_executed, @@GLOBAL.gtid_purged) AS lagging; | MySQL Replication Lag |
| gtid_setup.sql | CREATE USER 'repl'@'%' IDENTIFIED BY 'password'; | MySQL GTID-Based Replication |
Key takeaways
UUID() and NOW(), with zero error output to alert you.Interview Questions on This Topic
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?
Frequently Asked Questions
20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.
That's MySQL & PostgreSQL. Mark it forged?
12 min read · try the examples if you haven't