The idea: a history with a name and a position

A Redis replica is a copy of the master that is kept up to date by a replication stream: every write command the master executes is also sent, as it is, to its replicas. To know whether a replica that reconnects can simply continue, the master names its history with a replication ID (master_replid, 40 random hex characters, shortened on the page) and counts the bytes of the stream with the replication offset (master_repl_offset). The pair (replid, offset) marks an exact point in the data set's history. The master also keeps the end of the stream in a replication backlog, a circular buffer, so a replica that missed a little can get just that part again.

The master redis-1 has replid 8c1f3a, offset 12 and a backlog ring holding offsets 5 to 12. Three PSYNC requests: PSYNC 8c1f3a 10 is in the backlog and gets +CONTINUE with 3 commands; PSYNC 8c1f3a 3 was overwritten and gets +FULLRESYNC; PSYNC ? -1 from an empty server gets +FULLRESYNC
A replica asks to continue from (replid, offset + 1); only a known ID and an offset still in the backlog give a partial resync.

The page runs one master and two replicas. Numbers are scaled so they fit:

ThingReal RedisOn the page
offsetbytes of the RESP streamone step per command
repl-backlog-size1mb8 commands, drawn as a ring of 8 slots
replid40 hex charactersthe first 6, then …
REPLCONF ACKevery secondafter every Write (one second of clock)

The handshake

A replica configured with replicaof <host> <port> (or the command REPLICAOF) opens a connection to the master and goes through a short handshake:

  1. PING, to check that the master answers (+PONG), then AUTH if masterauth is set;
  2. REPLCONF listening-port 6379, so the master can list the replica in INFO replication, and REPLCONF capa eof capa psync2: the replica can read a diskless RDB (eof) and understands PSYNC2 (psync2);
  3. PSYNC <replid> <offset>: "I have history replid up to offset − 1; continue from offset." An empty server sends PSYNC ? -1.

Full resync

When the master cannot continue that history it replies +FULLRESYNC <replid> <offset> and sends a snapshot instead (Demo: first sync (full resync)):

  • With repl-diskless-sync no the master runs a BGSAVE: it forks, the child writes dump.rdb, and the file is sent when it is complete.
  • With repl-diskless-sync yes (the default since Redis 7.0) the child writes the RDB straight into the replica's socket. The master first waits repl-diskless-sync-delay (5 seconds) so that other replicas that arrive can share the same transfer.
  • The snapshot holds the data at the moment of the fork. Writes that arrive meanwhile are executed by the master and also put in that replica's output buffer (limited by client-output-buffer-limit replica 256mb 64mb 60).
  • The replica throws its old data away, loads the RDB and then receives the buffered writes followed by the live stream.
Time runs down between redis-3 on the left and redis-1 on the right: PING and +PONG, REPLCONF listening-port and capa, PSYNC ? -1, +FULLRESYNC 8c1f3a 6, fork and RDB at offset 6 while SET k7 and SET k8 go into the output buffer, the RDB is sent and loaded, the buffer with k7 and k8 is streamed, REPLCONF ACK 8
During a full resync the snapshot is frozen at the fork; the writes made while it travels wait in the replica's output buffer.

Partial resync and the backlog

The master answers +CONTINUE when two things hold: the replica's ID is the master's current replid (or its replid2, see below), and the requested offset is still inside the backlog. Then it sends only the missing part of the stream from the backlog, and the replica keeps all its data (Demo: short disconnect, partial resync).

The backlog is a ring: a new command overwrites the oldest one once the buffer is full. On the page, offset o lives in slot (o − 1) mod 8. If a replica is away while more than 8 commands are written, the offset it asks for has been overwritten and it needs a full resync again (Demo: backlog overflow forces full resync). In production, size repl-backlog-size as write rate × the longest disconnect you want to survive without a full copy; the backlog is freed after repl-backlog-ttl (3600 s) without any replica.

Replicas keep a backlog too, filled from the stream they receive. That is what lets a replica that becomes master serve partial resyncs.

The stream and REPLCONF ACK

Replication is asynchronous. The master executes a write, answers +OK and sends the command to the replicas without waiting for them. Each replica reports how far it got with REPLCONF ACK <offset> once per second; the master shows the difference as lag in INFO replication. The master also pings its replicas every repl-ping-replica-period (10 s), and either side drops the link after repl-timeout (60 s) of silence.

WAIT and min-replicas-to-write

WAIT numreplicas timeout blocks the client until at least numreplicas replicas have acknowledged all the client's previous writes, or until timeout milliseconds have passed. Instead of waiting up to a second for the next ack, the master sends REPLCONF GETACK * right away. WAIT returns the number of replicas that have the write. It does not undo anything: after a timeout the write is still there, on fewer copies (Demo: WAIT 1 for a replica ack).

min-replicas-to-write N together with min-replicas-max-lag S (10 s) makes the master refuse writes with -NOREPLICAS when fewer than N replicas are connected and acked within S seconds (Demo: min-replicas-to-write 1 refuses). It limits how long an isolated master keeps accepting writes that may later be lost, at the price of availability.

Failover and replid2 (PSYNC2)

When a replica is promoted with REPLICAOF NO ONE (by Sentinel, Cluster or an operator), it starts a new history: it picks a new replid. It does not forget the old one: it stores it as master_replid2 together with second_replid_offset = its offset + 1, the point where the two histories part. A replica of the old master that asks PSYNC <old id> <p> can continue partially if p ≤ second_replid_offset, because up to there both histories are the same. It receives +CONTINUE <new id> and moves the old ID to its own replid2 (Demo: failover keeps replid2).

Before: redis-1 master with replid 8c1f3a at offset 9; redis-2 and redis-3 at offset 8. After REPLICAOF NO ONE: redis-2 has replid 3ad7e9, replid2 8c1f3a and second_replid_offset 9. redis-3 sends PSYNC 8c1f3a 9, 9 is at most 9, gets +CONTINUE. The old master redis-1 sends PSYNC 8c1f3a 10, 10 is more than 9, gets +FULLRESYNC and loses k9
replid2 lets replicas that share the old history continue partially; a node that went further than the switch point needs a full copy.

Writes lost at failover

Because the master answers before the replicas have the write, a write can be acknowledged and still be lost: the master crashes after +OK but before the command left in the stream, and a replica without it is promoted (Demo: acked write lost at failover). When the old master comes back, it asks to continue from an offset beyond second_replid_offset; its history forked, so it gets a full resync, and the write disappears from it as well. WAIT reduces this window but does not close it: a failover can still promote a replica that did not ack.

See also how one Redis thread serves many connections, how Redis Sentinel fails over a master and Redis Cluster; the other Redis internals pages RDB and AOF persistence, the dict, encodings, sorted sets, expiry and eviction, streams and transactions; and, for the general picture, database replication, Kafka replication and copy-on-write fork (what BGSAVE relies on).

What the page leaves out

Offsets in bytes and real backlog sizes; AUTH/masterauth and TLS; the master's periodic PINGs and repl-timeout; replicas of replicas (sub-replicas share the master's stream and replid); repl-diskless-load on the replica; the rdb-channel replication of newer versions; client-output-buffer-limit killing a slow full sync; the SELECT commands and expiry handling inside the stream; replica-read-only and replica-serve-stale-data; and how Sentinel chooses the replica (priority, offset, run ID) and tells clients.