This error occurs when attempting write operations (SET, DEL, etc.) inside a Lua script executed with EVAL_RO or by a read-only user. Use EVAL for write operations or ensure your script only performs reads.
This error is raised by Redis when a Lua script attempts to execute write commands (such as SET, DEL, INCR, or even PUBLISH) while running in read-only mode. Redis enforces this restriction to maintain data consistency and proper replication behavior. Read-only scripts are executed using special command variants like EVAL_RO, EVALSHA_RO, or FCALL_RO. These variants are designed to route scripts to read replicas safely and can be killed without data loss using SCRIPT KILL. When Redis detects a write command within such a script, it immediately rejects the operation with this error. This behavior was introduced as part of Redis's scripting improvements in versions 7.0 and 7.2, where the distinction between read-only and write scripts became more strictly enforced. The restriction also applies when scripts are executed by users with read-only permissions or when connected to a read replica attempting write operations.
First, review your Lua script to identify all commands that modify data:
-- Example problematic script with EVAL_RO
EVAL_RO "redis.call('SET', KEYS[1], ARGV[1]); return redis.call('GET', KEYS[1])" 1 mykey myvalue
-- Error: ERR Write commands are not allowed from read-only scriptsCommon write commands to look for:
- Data modification: SET, DEL, INCR, DECR, APPEND
- List operations: LPUSH, RPUSH, LPOP, RPOP
- Set operations: SADD, SREM
- Hash operations: HSET, HDEL
- Publishing: PUBLISH, SPUBLISH
- Special cases: PFCOUNT (treated as write in scripts)
If your script needs to perform write operations, use the standard EVAL command:
# Wrong - using EVAL_RO with write commands
redis-cli EVAL_RO "redis.call('SET', 'counter', 1)" 0
# Correct - using EVAL for write operations
redis-cli EVAL "redis.call('SET', 'counter', 1)" 0In application code:
// Node.js with ioredis
// Wrong
await redis.eval_ro("redis.call('INCR', KEYS[1])", 1, 'mycounter');
// Correct
await redis.eval("redis.call('INCR', KEYS[1])", 1, 'mycounter');If you want to keep using EVAL_RO (for replica routing or killability), ensure your script only performs read operations:
-- Read-only script - safe for EVAL_RO
local value = redis.call('GET', KEYS[1])
local count = redis.call('HLEN', KEYS[2])
return {value, count}redis-cli EVAL_RO "local value = redis.call('GET', KEYS[1]); return value" 1 mykeyYou can also use the no-writes flag in Redis 7.0+ with shebang notation:
#!lua flags=no-writes
return redis.call('GET', KEYS[1])If you're connected to a read replica, switch to the master instance:
// Node.js configuration
const Redis = require('ioredis');
// Wrong - trying writes on replica
const replica = new Redis({
host: 'replica.redis.example.com',
port: 6379
});
// Correct - use master for writes
const master = new Redis({
host: 'master.redis.example.com',
port: 6379
});
await master.eval("redis.call('SET', KEYS[1], ARGV[1])", 1, 'mykey', 'myvalue');In Redis Cluster or Sentinel setups, ensure your client library routes write operations to the master automatically.
If using ACLs, verify your user has write permissions:
# Check current user permissions
ACL WHOAMI
ACL GETUSER myuser
# Create user with script write permissions
ACL SETUSER scriptuser on >password ~* +@all +eval
# Or specifically allow write commands needed
ACL SETUSER scriptuser on >password ~* +eval +set +del +incrIf the user is intentionally read-only, use a different user account for scripts requiring writes, or split your logic into separate read-only and write scripts.
Redis 7.0+ Breaking Changes:
Starting with Redis 7.0, the enforcement of read-only scripts became stricter. Commands like PUBLISH and PFCOUNT are now treated as write commands even though they don't modify data directly. This change ensures consistent behavior across clustered environments and proper replication.
Performance Benefits of EVAL_RO:
Read-only scripts executed with EVAL_RO have several advantages:
- Can run on read replicas, distributing load away from the master
- Can be killed with SCRIPT KILL without data loss concerns
- Don't fail with OOM errors when Redis is near memory limits
- Not blocked during write pauses (coordinated failovers)
Function Libraries (Redis 7.0+):
Consider using Redis Functions instead of EVAL for complex logic:
# Register a read-only function
FUNCTION LOAD "#!lua name=mylib
redis.register_function{function_name='myread', callback=function(keys, args) return redis.call('GET', keys[1]) end, flags={'no-writes'}}"
# Call it (automatically treated as read-only)
FCALL_RO myread 1 mykeyCluster Considerations:
In Redis Cluster, read-only scripts can be routed to replicas using the READONLY command, improving read scalability. Write scripts must always execute on master nodes and respect key slot restrictions.
ERR Unbalanced XREAD list of streams
How to fix "ERR Unbalanced XREAD list" in Redis
ERR syntax error
How to fix "ERR syntax error" in Redis
ConnectionError: Error while reading from socket
ConnectionError: Error while reading from socket in redis-py
ERR unknown command
How to fix ERR unknown command in Redis
Command timed out
How to fix 'Command timed out' in ioredis