MySQL Replication
MySQL replication allows data from one database server (the source, formerly called master) to be automatically copied to one or more database servers (replicas, formerly called slaves). It is the foundation of read scaling, high availability, and disaster recovery in MySQL deployments.
How Replication Works
The source records every data-changing event to its binary log (binlog).
The replica's I/O thread connects to the source and copies new binlog events into its own relay log.
The replica's SQL thread reads the relay log and replays the events on the replica's data.
Replication Topology Options
Topology | Description | Best for |
|---|---|---|
Single source / single replica | 1 source, 1 replica for failover | Simple HA with a hot standby |
Single source / multiple replicas | 1 source, N replicas for reads | Read scaling — distribute SELECT load |
Chain (cascading) replication | Source -> Replica A -> Replica B | Distributing replication across networks/regions |
Multi-source replication | One replica receives from multiple sources | Data consolidation, merging shards |
Circular (multi-master) | Source A <-> Source B both replicate to each other | Active-active setups (high complexity, risk of conflict) |
# Single source / multiple replicas (most common read-scaling pattern) # # [Source] # / \ # [Replica1] [Replica2] # # Writes go to Source only; reads distributed across replicas # Each replica has server_id = 2, 3, etc.
Binary Log Format Comparison
Format | What is Logged | Log Size | Safety | Best for |
|---|---|---|---|---|
STATEMENT | The SQL statement itself | Small | Unsafe for non-deterministic functions (NOW(), UUID()) | Compactness on simple workloads |
ROW | Actual row data (before + after images) | Large | Exact and safe for all statements | Production default (MySQL 8.0 default) |
MIXED | STATEMENT by default, ROW for unsafe statements | Medium | Good balance, can be unpredictable | Legacy setups migrating to ROW |
ROW format, which is the safest choice for most production deployments. ROW format replication is immune to non-deterministic function drift.GTID Deep-Dive
GTID (Global Transaction Identifier) assigns a globally unique ID to every committed transaction across the entire replication topology. This makes failover and replica re-pointing dramatically simpler.
-- GTID format: source_uuid:transaction_id_range -- Example: 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100 -- Meaning: transactions 1 through 100 from source with UUID 3E11FA47... -- Check the GTIDs executed on this server SHOW VARIABLES LIKE 'gtid_executed'; -- Check the GTID mode values SHOW VARIABLES LIKE 'gtid_mode'; -- OFF = GTID disabled -- OFF_PERMISSIVE = Accepts both GTID and non-GTID transactions (migration step 1) -- ON_PERMISSIVE = Generates GTIDs, accepts both (migration step 2) -- ON = Full GTID mode (required for GTID replication) -- Switching from position-based to GTID replication (online migration, MySQL 5.7.6+) -- Step 1: source and all replicas SET @@GLOBAL.GTID_MODE = OFF_PERMISSIVE; -- Step 2: SET @@GLOBAL.GTID_MODE = ON_PERMISSIVE; -- Step 3: wait for Ongoing_anonymous_transaction_count = 0 SHOW STATUS LIKE 'Ongoing_anonymous_transaction_count'; -- Step 4: SET @@GLOBAL.ENFORCE_GTID_CONSISTENCY = ON; SET @@GLOBAL.GTID_MODE = ON;
Setting Up Replication (MySQL 8.0+)
Step 1 — Configure the source in my.cnf:
[mysqld] server_id = 1 log_bin = /var/log/mysql/mysql-bin.log binlog_format = ROW binlog_row_image = MINIMAL # reduce log size while staying safe gtid_mode = ON enforce_gtid_consistency = ON
Step 2 — Configure the replica in my.cnf:
[mysqld] server_id = 2 gtid_mode = ON enforce_gtid_consistency = ON read_only = ON super_read_only = ON # blocks even SUPER users from writing
Step 3 — Create a replication user on the source:
CREATE USER 'replicator'@'%' IDENTIFIED WITH caching_sha2_password BY 'str0ngP@ss!'; GRANT REPLICATION SLAVE ON *.* TO 'replicator'@'%'; FLUSH PRIVILEGES;
Step 4 — Take a consistent backup and restore on replica:
# On source — consistent dump with GTID info mysqldump --single-transaction --set-gtid-purged=ON --all-databases -u root -p > full_backup.sql # Transfer to replica, then restore mysql -u root -p < full_backup.sql
Step 5 — Connect the replica to the source:
-- MySQL 8.0.23+ syntax CHANGE REPLICATION SOURCE TO SOURCE_HOST = '192.168.1.10', SOURCE_PORT = 3306, SOURCE_USER = 'replicator', SOURCE_PASSWORD = 'str0ngP@ss!', SOURCE_AUTO_POSITION = 1; -- GTID auto-positioning (no manual file/offset needed) START REPLICA;
Monitoring: SHOW REPLICA STATUS
SHOW REPLICA STATUSG
*************************** 1. row ***************************
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 0
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Executed_Gtid_Set: 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5023Replica Lag — Causes and Solutions
Seconds_Behind_Source measures how far behind the replica is. It is the timestamp difference between the current SQL thread event and the current clock time — not necessarily the number of seconds of data missed.
Heavy write load on source that the single SQL thread cannot keep up with
Long-running queries on the replica blocking the SQL thread
Network latency between source and replica
Replica hardware (I/O, CPU) weaker than source
Large transactions (e.g. a bulk UPDATE of millions of rows) — single event, long to replay
# my.cnf — enable multi-threaded parallel replication (MySQL 5.7+) [mysqld] replica_parallel_workers = 8 # number of parallel SQL applier threads replica_parallel_type = LOGICAL_CLOCK # better parallelism than DATABASE mode replica_preserve_commit_order = ON # maintain commit order for GTID consistency
pt-heartbeat from Percona Toolkit for more accurate replica lag measurement than Seconds_Behind_Source, which can show 0 even when the replica is behind on large transactions.Semi-Synchronous Replication
By default MySQL replication is asynchronous: the source commits and moves on without waiting for any replica. With semi-synchronous replication, the source waits until at least one replica acknowledges receiving (but not necessarily applying) the events before returning success to the client. This prevents data loss on source failure at the cost of added latency.
-- Install plugin on BOTH source and replica INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so'; INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so'; -- Enable on source SET GLOBAL rpl_semi_sync_source_enabled = 1; -- Fall back to async after 1 second if no replica acknowledges (prevents blocking forever) SET GLOBAL rpl_semi_sync_source_timeout = 1000; -- Require at least 1 replica to ack (increase for stricter durability) SET GLOBAL rpl_semi_sync_source_wait_for_replica_count = 1; -- Enable on replica SET GLOBAL rpl_semi_sync_replica_enabled = 1; -- Restart the I/O thread to activate semi-sync STOP REPLICA IO_THREAD; START REPLICA IO_THREAD; -- Monitor semi-sync status SHOW STATUS LIKE 'Rpl_semi_sync%';
Common Replication Errors and Fixes
Error | Error Number | Common Cause | Fix |
|---|---|---|---|
Duplicate entry | 1062 | Row already exists on replica (perhaps inserted directly) | Skip event or re-sync replica |
Row not found | 1032 | Row was deleted on replica but source tries to update it | Skip event or re-sync replica |
Access denied | 1045 | Replication user password changed or revoked | Update CHANGE REPLICATION SOURCE password |
Relay log corrupted | 1594 | Disk issue or crash mid-write | STOP REPLICA; RESET REPLICA; re-provision |
GTID skip needed | N/A | Specific GTID transaction needs to be skipped | Inject empty transaction for that GTID |
-- Skip a single erring event (position-based replication) STOP REPLICA; SET GLOBAL sql_replica_skip_counter = 1; START REPLICA; -- Skip a specific GTID transaction (GTID replication) -- Find the problematic GTID from SHOW REPLICA STATUS: Last_SQL_Error_Timestamp STOP REPLICA; SET GTID_NEXT = '3E11FA47-71CA-11E1-9E33-C80AA9429562:101'; -- the failing GTID BEGIN; COMMIT; -- inject an empty transaction to mark this GTID as executed SET GTID_NEXT = 'AUTOMATIC'; START REPLICA; -- Verify replication resumed SHOW REPLICA STATUSG
Parallel Replication
# LOGICAL_CLOCK mode: transactions that committed in overlapping binary log groups # on the source can be applied in parallel on the replica. # This matches the source's concurrency more accurately than DATABASE mode. [mysqld] replica_parallel_workers = 8 replica_parallel_type = LOGICAL_CLOCK replica_preserve_commit_order = ON
-- Verify parallel workers are active SHOW PROCESSLIST; -- Should show multiple 'replica sql' threads -- Monitor parallel replication efficiency SHOW GLOBAL STATUS LIKE 'Replica_transactions_multi_threaded_retries'; -- High retries = conflicts between parallel workers; reduce workers or switch to DATABASE mode
Promoting a Replica to Source (Failover)
Stop writes to the current source (or it has already crashed).
On the chosen replica: verify Seconds_Behind_Source = 0 in SHOW REPLICA STATUS.
On the chosen replica: run STOP REPLICA; RESET REPLICA ALL;
On the chosen replica: run SET GLOBAL read_only = OFF; SET GLOBAL super_read_only = OFF;
Point all other replicas at the new source: CHANGE REPLICATION SOURCE TO SOURCE_HOST = 'new-source', SOURCE_AUTO_POSITION = 1;
Update application connection strings / load balancer / MySQL Router to point at the new source.
Monitor for replication lag on the remaining replicas.
Read Scaling with Replicas
# Application config example (Node.js / mysql2 pool cluster)
const pool = mysql.createPoolCluster();
pool.add('WRITE', {
host: 'db-source.internal',
user: 'app', password: 'secret', database: 'myapp'
});
pool.add('READ1', {
host: 'db-replica-1.internal',
user: 'app', password: 'secret', database: 'myapp'
});
pool.add('READ2', {
host: 'db-replica-2.internal',
user: 'app', password: 'secret', database: 'myapp'
});Key Replication Variables Reference
Variable | Scope | Description |
|---|---|---|
server_id | Both | Unique integer ID — must differ on every server in the topology |
log_bin | Source | Path/name for binary log files — enables binlogging |
binlog_format | Source | ROW | STATEMENT | MIXED — ROW is safest |
gtid_mode | Both | ON to enable GTID replication |
enforce_gtid_consistency | Both | ON — prevent statements incompatible with GTID |
read_only | Replica | Prevents accidental writes from non-SUPER users |
super_read_only | Replica | Blocks even SUPER users from writing |
replica_parallel_workers | Replica | Number of parallel SQL threads (0 = single-threaded) |
replica_parallel_type | Replica | LOGICAL_CLOCK or DATABASE — LOGICAL_CLOCK recommended |
rpl_semi_sync_source_enabled | Source | 1 to enable semi-synchronous replication |
binlog_row_image | Source | FULL/MINIMAL/NOBLOB — MINIMAL reduces log size |
Replication Filters
You can replicate only specific databases or tables using filter options in my.cnf on the replica:
# my.cnf on replica — replicate only specific databases or tables replicate-do-db = myapp # Only replicate this database replicate-ignore-db = test # Skip this database entirely replicate-do-table = myapp.orders # Only replicate this table replicate-ignore-table = myapp.sessions # Skip this table
Multi-Source Replication
MySQL 5.7+ allows a single replica to receive replication from multiple sources simultaneously — useful for data consolidation or merging sharded databases.
-- Add a second source to an existing replica CHANGE REPLICATION SOURCE TO SOURCE_HOST = '192.168.1.20', SOURCE_PORT = 3306, SOURCE_USER = 'replicator', SOURCE_PASSWORD = 'pass', SOURCE_AUTO_POSITION = 1 FOR CHANNEL 'shard2'; START REPLICA FOR CHANNEL 'shard2'; -- Monitor each channel separately SHOW REPLICA STATUS FOR CHANNEL 'shard2'G -- Stop a specific channel STOP REPLICA FOR CHANNEL 'shard2';
Best Practices
Always use GTID mode for new deployments — it simplifies failover dramatically.
Set read_only = ON and super_read_only = ON on all replicas to prevent accidental writes.
Monitor Seconds_Behind_Source continuously and alert when it exceeds your SLA threshold.
Test failover procedures regularly — a plan you have not practised is no plan at all.
Keep replica MySQL version equal to or newer than the source version (never older).
Enable binlog_row_image = MINIMAL to reduce ROW format log size while remaining safe.
Use semi-synchronous replication when data loss on source failure is unacceptable.
Monitoring Replication Health
-- Most important replication health checks SHOW REPLICA STATUSG -- Key fields and their healthy values: -- Replica_IO_Running: Yes -- Replica_SQL_Running: Yes -- Seconds_Behind_Source: 0 (or close to 0) -- Last_IO_Error: (empty) -- Last_SQL_Error: (empty) -- Monitor via performance_schema (MySQL 8.0) SELECT channel_name, service_state, LAST_ERROR_MESSAGE, LAST_ERROR_TIMESTAMP FROM performance_schema.replication_applier_status_by_worker; -- Compare GTID sets: what has the replica executed vs what source has committed? -- On source: SELECT @@GLOBAL.GTID_EXECUTED AS source_gtids; -- On replica: SELECT @@GLOBAL.GTID_EXECUTED AS replica_gtids; -- Find which transactions replica is missing: SELECT GTID_SUBTRACT( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-1000', -- source '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-990' -- replica ) AS missing_gtids; -- Returns: 3E11FA47-71CA-11E1-9E33-C80AA9429562:991-1000
Replication Quick Reference
Topic | Command / Setting |
|---|---|
Start/stop replication | START REPLICA; / STOP REPLICA; |
Check status | SHOW REPLICA STATUS\G |
View binlogs on source | SHOW BINARY LOGS; |
GTID mode | gtid_mode = ON, enforce_gtid_consistency = ON |
Prevent replica writes | read_only = ON, super_read_only = ON |
Parallel workers | replica_parallel_workers = 8, replica_parallel_type = LOGICAL_CLOCK |
Semi-sync enable | rpl_semi_sync_source_enabled = 1 (source), rpl_semi_sync_replica_enabled = 1 (replica) |
Skip error (position) | STOP REPLICA; SET GLOBAL sql_replica_skip_counter = 1; START REPLICA; |
Skip GTID error | STOP REPLICA; SET GTID_NEXT = "uuid:N"; BEGIN; COMMIT; SET GTID_NEXT = AUTOMATIC; START REPLICA; |
Purge old binlogs | PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY); |
Binary Log Management
Binary logs grow continuously and must be managed proactively. The source server accumulates binlog files that replicas have already consumed. Purging old files frees disk space.
-- List current binary log files and their sizes SHOW BINARY LOGS; -- Set automatic expiry (MySQL 8.0: binlog_expire_logs_seconds) SET GLOBAL binlog_expire_logs_seconds = 604800; -- 7 days in seconds -- my.cnf: binlog_expire_logs_seconds = 604800 -- Manual purge: delete logs older than a specific date PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY); -- Manual purge: delete logs up to a specific file PURGE BINARY LOGS TO 'mysql-bin.000050'; -- Check how much disk the binary logs are using SELECT SUM(file_size) / 1024 / 1024 AS total_binlog_mb FROM information_schema.FILES WHERE file_type = 'BINARY LOG'; -- IMPORTANT: before purging, verify all replicas have consumed those logs -- Check each replica's relay log position to ensure it is past the logs you will delete SHOW REPLICA STATUSG -- check Executed_Gtid_Set or Read_Master_Log_Pos