Fix Redis 'READONLY You can't write against a read only replica' Error

intermediate๐Ÿ”ด Redis2026-05-16| Redis 4.x / 5.x / 6.x / 7.x on Linux, Docker, Kubernetes โ€” any replication setup (standalone replica, Sentinel, Redis Cluster)

Error Message

READONLY You can't write against a read only replica.
#redis#replica#slave-read-only#replication#config

What Just Happened

You ran a write command โ€” SET, HSET, LPUSH, or anything that modifies data โ€” and Redis threw back:

READONLY You can't write against a read only replica.

Your client connected to a replica node (formerly called a slave) instead of the primary. Replicas are read-only by default โ€” full stop. Any write attempt gets rejected immediately, no exceptions.

The fix depends on why your client ended up talking to the wrong node in the first place.

Confirm You're Actually on a Replica

Don't guess. Run this before touching anything:

redis-cli -h your-redis-host -p 6379 INFO replication

Check the role field in the output:

# Replication
role:slave
master_host:192.168.1.10
master_port:6379
master_link_status:up

Seeing role:slave or role:replica (Redis 5+) confirms the problem. Write down the master_host value โ€” that's where your writes need to go.

Common Causes

  • Hardcoded replica IP in your app config or connection string
  • Sentinel failover happened โ€” the old primary became a replica, but your app never reconnected
  • Load balancer routing writes to replicas โ€” round-robin with no read/write splitting
  • Wrong port โ€” Sentinel listens on 26379, not the Redis default 6379
  • Cluster client misconfigured โ€” not following MOVED redirects, landing on a replica within the shard

Fix 1 โ€” Point Your Client at the Primary

Most direct approach: grab the primary address and connect to it.

# Pull the primary host from any node
redis-cli -h your-redis-host -p 6379 INFO replication | grep master_host

# Connect directly
redis-cli -h 192.168.1.10 -p 6379
127.0.0.1:6379> SET foo bar
OK

Then update your application config to match:

# Python redis-py example
import redis
r = redis.Redis(host='192.168.1.10', port=6379)  # primary, not replica
r.set('foo', 'bar')

This works, but it's fragile. The moment a failover happens, you're back to square one. For anything in production, jump to Fix 2.

Fix 2 โ€” Use Sentinel So Your App Finds the Primary Automatically

Hardcoding the primary IP is asking for trouble. After a failover โ€” which Sentinel can trigger in under 30 seconds โ€” your app is pointing at a replica again.

Sentinel tracks the current primary and hands out the right address on demand:

# Python redis-py with Sentinel
from redis.sentinel import Sentinel

sentinel = Sentinel(
    [('sentinel1', 26379), ('sentinel2', 26379), ('sentinel3', 26379)],
    socket_timeout=0.1
)

# Always returns the current primary
master = sentinel.master_for('mymaster', socket_timeout=0.1)
master.set('foo', 'bar')

# Read-only connection to a replica
slave = sentinel.slave_for('mymaster', socket_timeout=0.1)
slave.get('foo')

Already using Sentinel but still hitting the error? Double-check that mymaster matches the name in your sentinel.conf:

redis-cli -h sentinel-host -p 26379 SENTINEL masters

A name mismatch silently causes connection failures that look like routing errors.

Fix 3 โ€” Temporarily Allow Writes on Replica (Not Recommended for Production)

Redis has a config option called replica-read-only no that drops the read-only guard. Sounds convenient. It isn't โ€” at least not for production.

Here's the problem: during replication sync, the primary sends a full dataset dump to the replica. Any data you wrote locally gets wiped. There's no merge. It's gone.

# Runtime change (resets on restart)
redis-cli -h replica-host CONFIG SET replica-read-only no

# Permanent (add to redis.conf)
replica-read-only no

Use this only on isolated replicas for one-off debugging or migration tasks. Never in a live setup where that data matters.

Fix 4 โ€” Redis Cluster: Enable Follow-Redirects in Your Client

Cluster mode adds a wrinkle. A write to the wrong shard returns MOVED, not READONLY. But if you hit a replica within the correct shard, you get READONLY. Two different errors, similar root cause.

Make sure your cluster client targets primaries for writes:

# Python redis-py cluster client
from redis.cluster import RedisCluster

rc = RedisCluster(
    host='redis-node-1',
    port=6379,
    read_from_replicas=False  # All ops go to primary nodes
)
rc.set('foo', 'bar')

Want read scaling through replicas? Set read_from_replicas=True โ€” but verify your client only routes reads that way, not writes.

Fix 5 โ€” After a Sentinel Failover, Force Reconnect

Apps that cache the primary's address get caught out after failover. The cached IP now points at a replica.

# Check who the current primary is
redis-cli -h sentinel-host -p 26379 SENTINEL get-master-addr-by-name mymaster
# Returns: 192.168.1.11 6379  โ† new primary post-failover

Restarting the app or flushing the connection pool clears the stale address. With a properly configured Sentinel client, this happens automatically โ€” no manual intervention needed.

Verify the Fix

Run a quick write/read test before calling it done:

redis-cli -h correct-primary-host -p 6379 SET test:write ok
# Expected: OK

redis-cli -h correct-primary-host -p 6379 GET test:write
# Expected: "ok"

# Clean up
redis-cli -h correct-primary-host -p 6379 DEL test:write

Then confirm the role one more time:

redis-cli -h correct-primary-host -p 6379 INFO replication | grep role
# Expected: role:master

Lessons Learned

  • Hardcoding the primary's IP is a time bomb โ€” use Sentinel or a cluster-aware client so failover is invisible to your app.
  • Separate your connection pools โ€” replicas handle reads, primary handles writes. Make that split explicit in your connection setup, not implicit.
  • Set up role-change alerts โ€” watch for role:slave on nodes that should be primary, or subscribe to Sentinel events: redis-cli -h sentinel-host -p 26379 SUBSCRIBE +switch-master.
  • Leave replica-read-only alone in production. It exists for a reason: replica data is ephemeral, and disabling the guard just means you'll lose writes silently during the next sync.

Related Error Notes