Skip to main content
MySQL · lesson 7 of 8
MySQL advanced Lesson 7 of 8

Also part of: Databases

Replication: A Second Server, and the read_only Gap Nobody Mentions

A real primary-replica setup, checked with SHOW REPLICA STATUS instead of assumed — plus a genuine surprise: read_only alone does not stop root from writing to a replica.

By SkillByExample · Published

mysql replication read-only binlog

Setting up replication is one CHANGE REPLICATION SOURCE TO statement. Trusting it is a separate question — and the first real attempt below fails before it even connects.

Connecting a replica: the error a tutorial’s copy-paste skips

-- on the replica, first attempt
CHANGE REPLICATION SOURCE TO
  SOURCE_HOST='172.17.0.3', SOURCE_PORT=3306,
  SOURCE_USER='repl', SOURCE_PASSWORD='replpass',
  SOURCE_LOG_FILE='binlog.000002', SOURCE_LOG_POS=13192;
START REPLICA;

SHOW REPLICA STATUS\G
Replica_IO_Running: Connecting
Last_IO_Error: Error connecting to source 'repl@172.17.0.3:3306'. This was attempt 1/10,
with a delay of 60 seconds between attempts. Message: Authentication plugin
'caching_sha2_password' reported error: Authentication requires secure connection.

The replication user authenticates with caching_sha2_password — MySQL 8’s default plugin — which refuses to complete a login over a plain connection without either TLS or the source’s public key. The fix is one more option on the same statement:

STOP REPLICA;
CHANGE REPLICATION SOURCE TO GET_SOURCE_PUBLIC_KEY=1;
START REPLICA;

SHOW REPLICA STATUS\G
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Last_IO_Error:
Last_SQL_Error:

Both threads running and no error text — the IO thread pulls binary log events from the primary, the SQL thread applies them, and each is reported separately because either one can fail independently of the other.

A real write, replicated, confirmed by polling — not assumed

-- on the primary
CREATE DATABASE repldemo;
CREATE TABLE repldemo.events (id INT PRIMARY KEY AUTO_INCREMENT, note VARCHAR(100));
INSERT INTO repldemo.events (note) VALUES ('first write on primary');
-- on the replica, checked in a short retry loop rather than a fixed sleep
SELECT COUNT(*) FROM repldemo.events;
Source_Host: 172.17.0.3
Seconds_Behind_Source: 0

id  note
1   first write on primary

Replication lag on a local, idle link is small enough that a fixed wait is guesswork — polling until the row count matches what was written on the primary is the same principle as checking Seconds_Behind_Source in production: confirm the actual state instead of trusting a timer.

The gap: read_only doesn’t mean what it sounds like

-- on the replica, started with --read-only=ON
INSERT INTO repldemo.events (note) VALUES ('should be rejected');
SELECT * FROM repldemo.events;
id  note
1   first write on primary
2   second write, timed
3   should be rejected

No error — the insert succeeded. read_only only restricts accounts without the SUPER privilege, and root has SUPER by default, so the setting that’s supposed to make this server a safe read replica did nothing for the account most likely to run a stray write.

SET GLOBAL super_read_only = ON;
INSERT INTO repldemo.events (note) VALUES ('should really be rejected now');
ERROR 1290 (HY000) at line 1: The MySQL server is running with the --super-read-only option
so it cannot execute this statement

super_read_only closes the gap — it blocks SUPER accounts too, which is the setting that actually makes a replica non-writable rather than just discouraging non-privileged clients.

Practice

1. With super_read_only enabled, confirm replicated writes from the primary still reach the replica.
-- on the primary
INSERT INTO repldemo.events (note) VALUES ('third write, after super_read_only');

-- on the replica, polled
SELECT * FROM repldemo.events;
id  note
1   first write on primary
2   second write, timed
3   third write, after super_read_only

The new row arrived despite super_read_only=ON — the replication applier thread is exempt from both read_only and super_read_only, which is exactly how a locked-down replica still receives the primary’s changes at all.

2. Check the current replication lag using the field meant for it, not row counts.
SHOW REPLICA STATUS\G
Seconds_Behind_Source: 0
Replica_SQL_Running_State: Replica has read all relay log; waiting for more updates

Seconds_Behind_Source: 0 combined with “waiting for more updates” is the real signal a replica is caught up — a growing number here, not an assumption from wall-clock time, is what actually indicates a replica is falling behind.

3. Explain why this setup used SOURCE_LOG_FILE and SOURCE_LOG_POS instead of SOURCE_AUTO_POSITION.

SOURCE_AUTO_POSITION=1 requires GTID mode (gtid_mode=ON), which is a server startup setting, not something togglable on a running instance without a restart and additional consistency guarantees. This primary was started with gtid_mode=OFF (the default), so the only available coordinate system is the classic binary-log file name and byte offset returned by SHOW BINARY LOG STATUS — which is why the exact file and position had to be read from the primary and copied into the replica’s CHANGE REPLICATION SOURCE TO statement by hand.

Next: backup and recovery — turning repldemo (and the rest of this database) into something that survives a mistake, and proving the restore actually works rather than trusting that it would.

Frequently Asked Questions

Why did connecting the replica fail the first time with a caching_sha2_password error?
caching_sha2_password, the default authentication plugin since MySQL 8.0, needs either an encrypted connection or the source server's RSA public key to complete a login without TLS configured. Adding GET_SOURCE_PUBLIC_KEY=1 to CHANGE REPLICATION SOURCE TO tells the replica to fetch that key over the unencrypted connection, which is enough for a local, already-trusted network — production setups more commonly configure SSL for replication instead.
Does setting read_only=ON on a replica actually stop anyone from writing to it?
Not by itself. read_only blocks accounts without the SUPER privilege, but root has SUPER by default, so root's own writes go through untouched, as this lesson shows directly. super_read_only additionally blocks SUPER accounts, which is the setting that actually makes a replica non-writable from any regular connection.
If super_read_only blocks writes, how does the replica ever apply the primary's changes?
The replication applier thread is exempt from both read_only and super_read_only — it's how replicated writes reach a replica's tables at all. This lesson confirms it directly: a row inserted on the primary after super_read_only was enabled still appeared on the replica.
What does Seconds_Behind_Source actually measure?
The gap between the current time and the timestamp of the last event the replica's SQL thread has applied — not queue depth or row count. A healthy replica keeping up shows 0; a replica falling behind shows a growing number, which is the first thing to check when replicated data looks stale.
Is this file-position-based setup the same as GTID-based replication?
No. This lesson uses classic binary-log file-and-position coordinates (SOURCE_LOG_FILE / SOURCE_LOG_POS), the original replication method and still common. GTID-based replication (gtid_mode=ON, SOURCE_AUTO_POSITION=1) tracks position with a globally unique transaction ID instead of a file offset, which simplifies failover, but requires the server to be started with GTID mode enabled — it isn't a setting you can toggle on a running server the way SOURCE_LOG_POS coordinates work.