MySQL replication lag means the replica is executing transactions that the primary wrote some time ago, and a broken replica is one whose SQL or I/O thread has stopped entirely. This guide shows you how to measure the real lag, read SHOW REPLICA STATUS field by field, clear the common SQL thread errors, and resync a replica from a fresh dump when it cannot catch up.
The commands below work on MySQL 8.0 and 8.4 and on MariaDB 10.x and 11.x. Where the two differ, the difference is called out.
Before you start
You need:
- Two servers: the primary and the replica, both reachable over SSH. On a Linux VPS you can confirm the IP and open a console from your VPS provider's control panel.
- Root or
sudoaccess on both, and themysqlclient installed. - The replication user's name and password on the primary (often
repl). - A maintenance window if you plan to resync, because the replica will be unusable during the restore.
Connect to the server you want to inspect:
mysql -u root -pNote: MySQL 8.0.22 renamed SHOW SLAVE STATUS to SHOW REPLICA STATUS and CHANGE MASTER TO to CHANGE REPLICATION SOURCE TO. The old names still work but print a deprecation warning. MariaDB keeps the older names.
Measure MySQL replication lag
Run this on the replica first. It is the single command that answers most questions:
SHOW REPLICA STATUS\GRead these fields:
Replica_IO_Running— is the replica pulling the binary log from the primary? Should beYes.Replica_SQL_Running— is the replica applying what it pulled? Should beYes.Seconds_Behind_Source— the lag estimate in seconds.Last_IO_ErrorandLast_SQL_Error— the reason a thread stopped.Retrieved_Gtid_SetandExecuted_Gtid_Set— with GTID replication, the gap between these two is exactly what is left to apply.Relay_Log_Space— how much relay log is still queued.
For per-worker detail on a multi-threaded replica:
SELECT * FROM performance_schema.replication_applier_status_by_worker\GTip: Seconds_Behind_Source can read 0 while the replica is genuinely behind, because it compares event timestamps rather than wall-clock progress. It also breaks down if the two servers' clocks differ. For a trustworthy number, write a heartbeat row to the primary every second and read its timestamp on the replica, or use pt-heartbeat from Percona Toolkit.
Fix a stopped SQL thread
A stopped SQL thread is the most common "broken replica". Look at Last_SQL_Error and match it to the cause:
1062 Duplicate entry— the row already exists on the replica.1032 Can't find record— the row is missing on the replica.1146 Table... doesn't exist— a schema change never reached the replica.1236 Could not find first log file name— the primary purged binary logs the replica still needs.
If the failing statement is a single transaction you have verified is safe to drop, skip it. Without GTID:
STOP REPLICA SQL_THREAD;
SET GLOBAL SQL_REPLICA_SKIP_COUNTER = 1;
START REPLICA SQL_THREAD;With GTID, take the transaction ID from Last_SQL_Error and commit an empty transaction with that GTID:
STOP REPLICA SQL_THREAD;
SET GTID_NEXT='aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:42';
BEGIN;
COMMIT;
SET GTID_NEXT='AUTOMATIC';
START REPLICA SQL_THREAD;Warning: Skipping a transaction makes the replica permanently diverge from the primary. Only skip when you can prove the change is already present or irrelevant, and schedule a checksum comparison (for example pt-table-checksum) afterwards. If you are unsure, resync instead.
If the error is 1236, skipping will not help — the binary logs are gone. Go to the resync section.
Resync a replica that fell too far behind
Warning: This replaces all data on the replica. Take a backup of anything unique to that server first, and expect the replica to be offline for the duration of the dump and restore.
- Stop replication on the replica and note the current state:
STOP REPLICA;
SHOW REPLICA STATUS\G- On the primary, take a consistent dump with the binary log position embedded:
mysqldump --single-transaction --source-data=2 \
--all-databases --routines --triggers --events \
-u root -p > /root/primary-dump.sqlNote: --source-data=2 exists in MySQL 8.0.26 and later; on older MySQL and on MariaDB use --master-data=2. Run mysqldump --help to confirm which flag your version accepts.
- Copy the dump to the replica:
scp /root/primary-dump.sql deploy@203.0.113.10:/root/- Import it on the replica:
mysql -u root -p < /root/primary-dump.sql- Point the replica at the primary and start it. With GTID:
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='203.0.113.10',
SOURCE_PORT=3306,
SOURCE_USER='repl',
SOURCE_PASSWORD='your-replication-password',
SOURCE_AUTO_POSITION=1;
START REPLICA;Without GTID, read SOURCE_LOG_FILE and SOURCE_LOG_POS from the comment block at the top of the dump and pass those instead of SOURCE_AUTO_POSITION.
Reduce lag on a busy primary
Once the replica is running again, stop it from drifting:
- Enable multi-threaded apply. Set
replica_parallel_workers(MySQL 8.0.26+;slave_parallel_workersearlier) to 4 or 8,replica_parallel_type=LOGICAL_CLOCKandreplica_preserve_commit_order=ONinmy.cnf, then restart MySQL. Check the file location with Where is my.cnf?. - Split bulk
DELETEandUPDATEstatements into batches. A single statement touching millions of rows blocks the applier for its whole duration. - Find statements that are slow on the replica itself with the slow query log, as described in Find slow queries in MySQL.
- On a replica with no downstream replicas,
sync_binlog=0andinnodb_flush_log_at_trx_commit=2reduce write pressure.
Warning: Lowering durability settings means a crash can lose the last few transactions on that server. Only do it on a replica you can rebuild, and never on the primary.
Check it works
On the replica:
SHOW REPLICA STATUS\GYou want Replica_IO_Running: Yes, Replica_SQL_Running: Yes, Seconds_Behind_Source: 0 and empty Last_IO_Error / Last_SQL_Error. Then prove data actually flows. On the primary:
CREATE DATABASE IF NOT EXISTS repltest;
CREATE TABLE repltest.t (id INT PRIMARY KEY AUTO_INCREMENT, note VARCHAR(50));
INSERT INTO repltest.t (note) VALUES ('lag-check');On the replica:
SELECT * FROM repltest.t;The row should appear within a second. Clean up on the primary with DROP DATABASE repltest; and confirm it disappears on the replica too.
Troubleshooting
1236 Could not find first log file name in binary log index file— the primary's binary logs were purged before the replica read them. Skipping cannot fix this; resync from a fresh dump and consider raisingbinlog_expire_logs_secondson the primary.1062 Duplicate entryor1032 Can't find record— the replica drifted, usually after a manual write or a skipped transaction. Skip the single transaction if you can prove it is safe, otherwise resync.Replica_IO_Running: Connectingand neverYes— the replica cannot reach the primary. Check that port 3306 is open between the two hosts, that the replication user still exists withREPLICATION SLAVEprivileges, and readLast_IO_Errorfor1045(bad credentials) or2003(connection refused).- Lag never reaches zero — look for a long-running query on the replica with
SHOW PROCESSLIST, a single-threaded applier, or a slow disk. CheckRelay_Log_Spaceto see whether the backlog is growing or shrinking. Seconds_Behind_Sourceis 0 but data is missing — clock skew between the hosts, or no writes happening on the primary. Verify with a heartbeat table rather than trusting the counter.
Next steps
- MySQL Point-in-Time Recovery Using Binary Logs — restore to a moment before a bad transaction reached the replica.
- Automating MySQL backups with mysqldump and cron — keep a dump ready so a resync is quick.
- Tune MySQL and MariaDB memory settings for a small VPS — give the applier threads enough buffer pool to keep up.
- MySQL command-line cheat sheet for server admins — the status and process commands you will reach for most often.
