Redis Lua Scripts
EVAL Basics
Scripts run atomically: no other command executes while a script is running. KEYS[n] and ARGV[n] are 1-indexed.
# EVAL script numkeys key [key ...] arg [arg ...]
EVAL "return 'hello'" 0
# Access keys and arguments
EVAL "return {KEYS[1], ARGV[1]}" 1 mykey myvalue
# Call Redis commands from Lua
EVAL "redis.call('SET', KEYS[1], ARGV[1]); return 1" 1 counter 42
# redis.call vs redis.pcall
# redis.call → raises error on failure (propagates to client)
# redis.pcall → returns error table, script continues
EVAL "
local v = redis.call('GET', KEYS[1])
if v then
return tonumber(v) + 1
end
return 1
" 1 mykey
Atomic Compare-and-Set
A classic use case: read-modify-write atomically without transactions.
# Atomic CAS: set new value only if current equals expected
EVAL "
local cur = redis.call('GET', KEYS[1])
if cur == ARGV[1] then
redis.call('SET', KEYS[1], ARGV[2])
return 1
end
return 0
" 1 mykey expected_value new_value
# Rate limiter: allow N requests per window
EVAL "
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, window)
end
if current > limit then
return 0
end
return 1
" 1 ratelimit:user:42 100 60
Distributed Lock with Lua
# Acquire lock (SET NX EX is atomic but Lua allows custom logic)
# SET key value NX EX seconds
SET lock:resource unique_token NX EX 30
# Release lock: only release if we own it (atomic check-and-delete)
EVAL "
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
" 1 lock:resource unique_token
# Extend lock TTL atomically
EVAL "
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('PEXPIRE', KEYS[1], ARGV[2])
else
return 0
end
" 1 lock:resource unique_token 30000
SCRIPT LOAD & EVALSHA
Load a script once and execute by its SHA1 hash to reduce network overhead.
# Load script into server cache, returns SHA1
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# Returns: "e0e1f9fabfa9d353eca4c6f67a4a3c35c92fa08d"
# Execute by SHA1
EVALSHA e0e1f9fabfa9d353eca4c6f67a4a3c35c92fa08d 1 mykey
# Check if script is cached
SCRIPT EXISTS sha1 sha2 sha3 # returns 0/1 per sha
# Flush all cached scripts
SCRIPT FLUSH
# Using EVALSHA in Node.js (ioredis)
const sha = await client.script("load", luaScript);
const result = await client.evalsha(sha, 1, "mykey");
Lua Best Practices
| Practice | Reason |
|---|---|
| Always pass keys via KEYS[] | Required for Redis Cluster key routing |
| Keep scripts short | Long scripts block Redis (single-threaded) |
| Use EVALSHA in production | Avoids resending script bytes every call |
| No I/O or sleep in scripts | Blocks the event loop |
| Use tonumber() / tostring() | Redis returns bulk strings; cast as needed |