Fix Redis "READONLY You Can’t Write Against a Read Only Replica"
Fix the Redis error READONLY You can’t write against a read only replica in Django and Celery: point clients at the primary endpoint, handle failovers with Sentinel or managed endpoints, and stop stale DNS and connection pools from writing to replicas.
Problem statement
After a Redis failover, a maintenance event, or a configuration change, writes start failing:
redis.exceptions.ReadOnlyError: You can't write against a read only replica.
Cache sets, session saves, Celery task publishing, and rate-limit counters all fail, while reads may keep working. The client is connected to a Redis node that is now a replica. Replicas accept reads but reject writes. Either the app was configured with a replica address, or a failover promoted another node and the app's existing connections, or its cached DNS answer, still point at the old primary, which was demoted.
- Check the node's role:
redis-cli -h <host> ROLEshowsmasterorslave. - Point Django and Celery at the primary endpoint your provider gives, not a node or reader endpoint.
- With Sentinel, use the Sentinel-aware client configuration so the app asks Sentinel for the current primary.
- Lower DNS caching and reconnect on
ReadOnlyErrorso pools drop stale connections. - On self-managed setups, make sure
replica-read-only yesreplicas are never in the app's config.
Step-by-step solution
1. Check which node the app talks to
redis-cli -h redis.internal -p 6379 ROLE
redis-cli -h redis.internal -p 6379 INFO replication | head -5
If the address in CACHES or CELERY_BROKER_URL answers slave, the app is pointed at a replica by configuration. If it answers master now but the errors happened earlier, a failover occurred and connections were stale.
2. Use the primary endpoint on managed Redis
Managed services give a stable name that follows the primary:
- ElastiCache (cluster mode disabled): the Primary endpoint, not the Reader endpoint or a node endpoint.
- Azure Cache for Redis: the main host name.
- Memorystore: the primary IP; the read endpoint is separate.
REDIS_URL = os.environ["REDIS_URL"] # rediss://master.myredis.abc123.use1.cache.amazonaws.com:6379
CACHES = {"default": {"BACKEND": "django_redis.cache.RedisCache", "LOCATION": f"{REDIS_URL}/1"}}
CELERY_BROKER_URL = f"{REDIS_URL}/0"
3. Configure Sentinel properly
With self-managed Sentinel, clients must ask Sentinel which node is primary. django-redis supports it:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://mymaster/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.SentinelClient",
"SENTINELS": [("sentinel-1", 26379), ("sentinel-2", 26379), ("sentinel-3", 26379)],
"CONNECTION_POOL_CLASS": "redis.sentinel.SentinelConnectionPool",
},
}
}
For Celery, use the sentinel:// broker URL with broker_transport_options = {"master_name": "mymaster"}.
4. Drop stale connections after failover
Even with the right endpoint, pooled connections opened before a failover remain attached to the old node. redis-py's Sentinel pool reconnects on ReadOnlyError. For plain connections, short SOCKET_TIMEOUT, health_check_interval, and a retry policy help:
"OPTIONS": {
"SOCKET_TIMEOUT": 2,
"CONNECTION_POOL_KWARGS": {"health_check_interval": 30, "retry_on_timeout": True},
}
Restarting web and worker processes after a failover clears pools if errors persist. Also keep DNS caching short: Java-style long caches do not apply to Python, but container platforms and local resolvers such as nscd can cache endpoints for minutes.
5. Self-managed replicas
If you run replicas yourself without Sentinel, a replica that was a primary before a manual switch keeps rejecting writes. Make sure the app config changes in the same step as the role change, ideally by using a DNS name or virtual IP you move with the primary.
Explanation
Replication in Redis is asynchronous and one-directional. Replicas are read-only by default (replica-read-only yes) so they cannot diverge from the primary. A failover changes which node is primary but cannot change where existing TCP connections point. Any client holding a connection to the old primary, now a replica, gets READONLY on its next write. Clients that discover the primary through Sentinel, Cluster, or a provider endpoint recover by reconnecting.
Edge cases / notes
- Redis Cluster. In cluster mode, clients get
MOVEDredirects instead, and need a cluster-aware client. Celery's Redis transport does not support Cluster mode. - Read replicas for cache reads. django-redis supports a primary and replica list for reads; writes always go to the first entry, which must be the primary endpoint.
- Maintenance windows. Managed providers fail over during patching. Expect this error once in a while unless clients handle it.
Next steps
- Restarts can also produce loading errors: see Fix Redis LOADING Dataset in Memory.
- Set up ElastiCache correctly with Use AWS ElastiCache Redis With Django.
- Check the rest with the Django Redis Production Checklist.
FAQ
Why do reads work but writes fail?
Replicas serve reads normally. Only write commands are rejected with READONLY.
Do I need Sentinel on ElastiCache?
No. ElastiCache handles failover and updates the primary endpoint. Use that endpoint instead of Sentinel.
Will restarting Django fix it?
Restarting clears stale connections, so it often does. The lasting fix is a correct endpoint and reconnect behavior.
Can Celery use Redis Cluster?
Not with the standard Redis transport. Use a single primary with replicas and failover, or a different broker.