The symptom
A replica never settles. master_link_status flips between up and down, the primary forks again and again, and sync_full keeps climbing. While this goes on you may also see:
- Reads on the replica failing with
LOADING Redis is loading the dataset in memory - Writes on the primary failing with
NOREPLICAS Not enough good replicas to write, ifmin-replicas-to-writeis set
One full sync after a new replica joins or a replica crashes is normal. Repeated full syncs for the same replica are the problem. Each cycle costs a fork, a full transfer and a full load, and you have no working replica while it runs.
Confirm the loop
Run these on the primary. Set REDISCLI_AUTH first if the server needs a password.
# Counters since the last restart: sample twice, a few minutes apart
redis-cli INFO stats | grep -E '^sync_(full|partial_ok|partial_err)'
# Role, replica state and backlog
redis-cli INFO replication
# Output buffer use per replica: watch omem and tot-mem grow
redis-cli CLIENT LIST TYPE replica
# Fork health
redis-cli INFO persistence | grep -E 'rdb_bgsave_in_progress|rdb_last_bgsave_status|rdb_last_cow_size'
On the replica:
redis-cli INFO replication | grep -E 'master_link_status|master_sync_in_progress|master_link_down_since_seconds|master_sync_perc'
redis-cli INFO persistence | grep -E '^loading'
Counter names can differ slightly by version, so check your build's output. master_sync_perc, for example, is missing on older builds.
Find the cause in the logs
The logs on both nodes usually name the cause. Exact wording varies by version, so search for the key phrase.
| Log line (or evidence) | Where | Cause | Fix |
|---|---|---|---|
scheduled to be closed ASAP for overcoming of output buffer limits | Primary | Writes during the sync overflowed the replica's output buffer | Raise client-output-buffer-limit replica |
Unable to partial resync with replica ... for lack of backlog | Primary | Replica was away longer than the backlog covers | Raise repl-backlog-size |
Partial resynchronization not accepted: Replication ID mismatch | Primary | Replica's replication ID is unknown, for example after a primary restart | One full sync is expected; investigate only if it repeats |
Timeout receiving bulk data from MASTER | Replica | Transfer stalled for longer than repl-timeout | Raise repl-timeout; check network and primary disk |
Disconnecting timedout replica | Primary | Replica went silent, often during a slow transfer or load | Raise repl-timeout; check the replica's disk and CPU |
Can't save in background: fork: Cannot allocate memory | Primary | Fork failed | Set vm.overcommit_memory = 1; free host memory |
Replica process killed, Out of memory in dmesg | Replica | Replica host is too small for the dataset | Resize it to match the primary |
# Default paths vary by package; check the logfile setting if nothing matches
grep -E 'output buffer limits|partial resync|Replication ID mismatch|timedout replica|Cannot allocate' /var/log/redis/redis-server.log | tail -50
# On the replica
grep 'Timeout receiving' /var/log/redis/redis-server.log | tail
dmesg -T | grep -iE 'out of memory|oom-kill' | tail
Why the buffer limit causes a loop
During a full sync the primary keeps every new write for that replica in its output buffer. The buffer grows from the start of the RDB snapshot until the replica has finished loading it.
The default limit is replica 256mb 64mb 60. The connection is closed if the buffer passes 256 MB, or stays above 64 MB for 60 seconds. A large dataset takes minutes to transfer and load, so a moderate write rate can trip the limit every time. The replica reconnects, the backlog can't cover the gap, and the sync starts over.
Size the limits from your numbers
Measure the replication write rate on the primary:
a=$(redis-cli INFO replication | tr -d '\r' | awk -F: '/^master_repl_offset/{print $2}')
sleep 60
b=$(redis-cli INFO replication | tr -d '\r' | awk -F: '/^master_repl_offset/{print $2}')
echo "$(( (b - a) / 60 )) bytes/sec"
Measure it at peak, and also while the batch jobs run. Take the full sync duration from the replica's log: the time from the sync starting to MASTER <-> REPLICA sync: Finished with success. If a sync never finishes, estimate it as snapshot time, plus RDB size divided by network throughput, plus load time.
Then calculate:
- Output buffer limit: write rate x full sync duration, plus a margin. At 5 MB/s and a 10-minute sync, the buffer needs about 3 GB, so set 4 GB.
- Backlog: write rate x the longest disconnect you want to survive without a full sync. At 5 MB/s and 5 minutes, that's about 1.5 GB.
Keep the buffer's hard limit above repl-backlog-size.
Stop the loop
Warning
Bigger buffers use real memory. Replica output buffers are not counted against
maxmemoryfor eviction, so they can push the host into the OOM killer, on top of copy-on-write growth from the fork. Check free host memory against your calculated limit andrdb_last_cow_sizebefore you raise anything.
Apply the changes on the primary. Both settings take effect without a restart.
redis-cli CONFIG SET client-output-buffer-limit "replica 4gb 2gb 300"
redis-cli CONFIG SET repl-backlog-size 1610612736
# Only if the logs show timeouts
redis-cli CONFIG SET repl-timeout 300
# Save to redis.conf so a restart keeps them
redis-cli CONFIG REWRITE
Apply the same settings on the replicas, because any of them can be promoted. repl-timeout has to be set on both sides, as each side enforces its own value. Sentinel and Cluster do not sync these settings between nodes.
Then lower the load while the sync catches up:
- Pause batch jobs and bulk imports. This shrinks both the buffer growth and the copy-on-write memory.
- Leave the replica alone. Restarting it starts the sync from zero.
- Keep clients off the replica until
master_link_status:up. It returnsLOADINGerrors during the load. - Watch the buffer and the sync from the primary:
watch -n 10 "redis-cli CLIENT LIST TYPE replica; redis-cli INFO memory | grep -E 'mem_clients_slaves|mem_total_replication_buffers|mem_fragmentation_ratio'"
mem_total_replication_buffers exists on 7.0 and later only.
Caution
Don't fail over to a looping replica. Before any promotion, confirm
master_link_status:upand a replica offset close to the primary'smaster_repl_offset. A replica that hasn't finished loading may hold old data or none at all, and promoting it loses writes.
Shorten the sync itself
| Setting | What it does | Notes |
|---|---|---|
repl-diskless-sync yes | Streams the RDB over the socket without writing it to disk | Default on 7.0+, off before; helps when disks are slow and networks are fast |
repl-diskless-load swapdb | Loads directly from the socket into a temporary dataset, keeping the old one until the load succeeds | Default is disabled; needs memory for both copies, so check first |
ElastiCache and other managed services
You can't run CONFIG SET on most managed services. On Amazon ElastiCache:
repl-backlog-sizecan be changed in the parameter group.client-output-buffer-limit-replica-hard-limit,-soft-limitand-soft-secondsare listed as not modifiable, so you can't apply the buffer fix above.reserved-memory-percent(default 25) sets aside memory for sync overhead. If nodes run tight during sync, keep it at the default or higher.
If replicas keep failing to sync on ElastiCache, reduce the write rate during the sync or scale up to a larger node type.
Prevent it next time
- Alert when
sync_fullincreases outside a planned change. A few per week is normal; several per hour is a loop. - Alert on
master_link_status:downon any replica for longer than your normal load time. - Recalculate the buffer and backlog when the dataset grows or write rates change. Limits that were fine at 10 GB will fail at 50 GB.
- Test a full sync of a replica at peak write load before you rely on it for failover.
- Keep replica hosts the same size as the primary.