EVAL-&-EVALSHA
Lua scripts run atomically on the Redis server. Read values, make decisions, write results - all without another client interfering. The ultimate tool for complex operations.
You'll Learn
- Running scripts with EVAL
- Caching scripts with EVALSHA
- Passing KEYS and ARGV
- Common Lua patterns for Redis
See Your Data, Not Terminal Text
Redimo visualizes every Redis data type beautifully. Edit inline, undo mistakes, stay safe in production.
1. Why Lua Scripting?
Transactions (MULTI/EXEC) batch commands but can't use results mid-execution. Lua scripts can read a value, make a decision, and write - all atomically.
The Problem Lua Solves
# Without Lua - race condition possible
val = GET counter
if val < 100:
INCR counter # Another client might INCR between GET and INCR
# With Lua - atomic read-modify-write
EVAL "
local val = redis.call('GET', KEYS[1])
if tonumber(val) < 100 then
return redis.call('INCR', KEYS[1])
end
return nil
" 1 counter2. EVAL: Run a Script
EVAL takes a Lua script, the number of keys, and arguments. The script accesses keys via KEYS[n] and other arguments via ARGV[n].
EVAL Syntax
EVAL "script" numkeys key1 key2 ... arg1 arg2 ...
# Example: Get and delete (atomic pop)
EVAL "
local val = redis.call('GET', KEYS[1])
redis.call('DEL', KEYS[1])
return val
" 1 mykey
# Example: Conditional increment
EVAL "
local current = tonumber(redis.call('GET', KEYS[1]) or 0)
if current < tonumber(ARGV[1]) then
return redis.call('INCR', KEYS[1])
end
return current
" 1 counter 100KEYS Array
KEYS[1]- First key argumentKEYS[2]- Second key argument- Used for all Redis key names
- Required for cluster compatibility
ARGV Array
ARGV[1]- First non-key argumentARGV[2]- Second non-key argument- Used for values, thresholds, etc.
- Strings - convert with tonumber()
3. EVALSHA: Cached Scripts
Sending the full script every time wastes bandwidth. EVALSHA uses a SHA1 hash of the script - Redis looks it up in its script cache.
Script Caching Flow
# First, load the script and get its SHA1
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
"a42059b356c875f0717db19a51f6aaa9161e77a2"
# Now use EVALSHA with the hash
EVALSHA a42059b356c875f0717db19a51f6aaa9161e77a2 1 mykey
"myvalue"
# If script not in cache (server restarted):
EVALSHA a42059b35... 1 mykey
(error) NOSCRIPT No matching script
# Fallback to EVAL (re-caches automatically)
EVAL "return redis.call('GET', KEYS[1])" 1 mykeyClient Libraries Handle This
4. Lua Basics for Redis
You don't need to master Lua. Here's enough to be dangerous:
Lua Quick Reference
-- Variables
local name = "value" -- Always use 'local'
local num = tonumber("42")
-- Conditionals
if condition then
-- do something
elseif other then
-- do other
else
-- default
end
-- Loops
for i = 1, 10 do
-- i goes 1, 2, 3... 10
end
for i, v in ipairs(array) do
-- iterate array
end
-- Tables (arrays/objects)
local arr = {"a", "b", "c"} -- arr[1] = "a" (1-indexed!)
local obj = {name = "John", age = 30}
-- String concat
local full = "Hello " .. name
-- Redis calls
redis.call('SET', 'key', 'value') -- Raises error on failure
redis.pcall('SET', 'key', 'value') -- Returns error, doesn't raise5. Common Patterns
Rate Limiting
-- Rate limit: max 100 requests per minute
EVAL "
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = tonumber(redis.call('GET', key) or 0)
if current >= limit then
return 0 -- Rate limited
end
redis.call('INCR', key)
if current == 0 then
redis.call('EXPIRE', key, window)
end
return limit - current - 1 -- Remaining requests
" 1 ratelimit:user:1001 100 60Atomic Get-and-Set with Condition
-- Only update if current value matches expected
EVAL "
local current = redis.call('GET', KEYS[1])
if current == ARGV[1] then
redis.call('SET', KEYS[1], ARGV[2])
return 1
end
return 0
" 1 mykey "expected" "newvalue"Distributed Lock
-- Acquire lock with expiration
EVAL "
if redis.call('EXISTS', KEYS[1]) == 0 then
redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2])
return 1
end
return 0
" 1 lock:resource worker-123 30
-- Release lock (only if we own it)
EVAL "
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0
" 1 lock:resource worker-123Leaderboard Update
-- Add score only if higher than current
EVAL "
local current = redis.call('ZSCORE', KEYS[1], ARGV[1])
local newscore = tonumber(ARGV[2])
if current == false or tonumber(current) < newscore then
redis.call('ZADD', KEYS[1], newscore, ARGV[1])
return newscore
end
return current
" 1 leaderboard user:1001 1506. Atomicity and Blocking
Scripts run atomically but also block Redis. Long scripts freeze everything.
Don't Do This
- • Loop over thousands of keys
- • Complex string parsing in Lua
- • Infinite loops (Redis kills after timeout)
- • Heavy computation
Do This Instead
- • Keep scripts short and focused
- • Batch operations on app side
- • Use script for atomicity, not computation
- • Profile with SLOWLOG
lua-time-limit
lua-time-limit config (default 5 seconds). Scripts running longer trigger warnings and can be killed with SCRIPT KILL. This is a safety net, not a feature to rely on.7. Debugging Scripts
Using redis.log()
EVAL "
local val = redis.call('GET', KEYS[1])
redis.log(redis.LOG_WARNING, 'Got value: ' .. tostring(val))
return val
" 1 mykey
-- Check Redis server logs for output
-- LOG_DEBUG, LOG_VERBOSE, LOG_NOTICE, LOG_WARNINGScript Management
-- Check if script is cached
SCRIPT EXISTS a42059b356c875f0717db19a51f6aaa9161e77a2
1) (integer) 1 -- Yes, cached
-- Flush all scripts from cache
SCRIPT FLUSH
-- Kill running script (if stuck)
SCRIPT KILL8. Cluster Considerations
In Redis Cluster, all keys accessed by a script must be on the same node (same hash slot).
Cluster-Safe Scripts
-- Good: Single key
EVAL "return redis.call('GET', KEYS[1])" 1 mykey
-- Good: Keys with same hash tag
EVAL "
local a = redis.call('GET', KEYS[1])
local b = redis.call('GET', KEYS[2])
return a .. b
" 2 {user:1001}:name {user:1001}:email
-- {user:1001} forces same slot
-- Bad: Keys might be on different nodes
EVAL "..." 2 user:1001 user:1002 -- May fail in clusterHash Tags
{tag} in key names to ensure related keys land on the same node. Only the part inside braces is hashed: {user:1001}:name and {user:1001}:email always go to the same slot.9. Client Integration
Node.js (ioredis)
const redis = new Redis();
// Define command
redis.defineCommand('ratelimit', {
numberOfKeys: 1,
lua: `
local current = tonumber(redis.call('GET', KEYS[1]) or 0)
if current >= tonumber(ARGV[1]) then
return 0
end
redis.call('INCR', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[2])
return 1
`
});
// Use it
const allowed = await redis.ratelimit('limit:user:1001', 100, 60);Python (redis-py)
import redis
r = redis.Redis()
script = r.register_script("""
local current = tonumber(redis.call('GET', KEYS[1]) or 0)
if current >= tonumber(ARGV[1]) then
return 0
end
redis.call('INCR', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[2])
return 1
""")
# Use it
allowed = script(keys=['limit:user:1001'], args=[100, 60])Quick Reference
| Command | Purpose |
|---|---|
EVAL script numkeys ... | Run Lua script |
EVALSHA sha numkeys ... | Run cached script by SHA |
SCRIPT LOAD script | Cache script, return SHA |
SCRIPT EXISTS sha ... | Check if scripts cached |
SCRIPT FLUSH | Clear script cache |
SCRIPT KILL | Kill running script |
Start Scripting
Lua scripts unlock atomic read-modify-write operations that transactions can't do.
Master them and see your data with Redimo.