Redis Lua Scripts
Running atomic server-side logic with EVAL and functions, the determinism rules, and why a long script is an outage.
A Lua script runs atomically on the server: no other command executes while it does. That makes it the cleanest way to express read-modify-write logic in Redis.
EVAL "return redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2])" 1 mykey myvalue 60Keys must be declared
Pass every key the script touches through KEYS, never hard-coded inside the script body:
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end" 1 lock:orders my-tokenThis is not a style rule. Redis Cluster routes a script by its declared keys; a script that accesses an undeclared key may run on the wrong node and behave incorrectly. All declared keys must also hash to the same slot.
Loading and reuse
SCRIPT LOAD "return redis.call('INCR', KEYS[1])"
> "e0e1f9fabfc9d4800c877a703b823ac0578ff831"
EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff831 1 counter:page-viewsEVALSHA sends the hash rather than the body. Clients should fall back to EVAL on a NOSCRIPT
error, because the script cache is cleared by restarts and by SCRIPT FLUSH.
Redis 7 adds Functions (FUNCTION LOAD), which persist across restarts and replicate as part of
the dataset — a better fit than scripts for logic that is part of the application's contract.
Determinism
Scripts are replicated by effect in modern versions, but determinism still matters for correctness and for the constraints Redis enforces:
- Use
redis.call('TIME')rather than Lua's clock if you need time, and be aware it makes the script non-deterministic in older replication modes. - Do not iterate over unordered collections and write based on the order —
SMEMBERShas no guaranteed order. - Never generate random values inside a script without seeding from an argument.
Scripts block everything
A worthwhile use: rate limiting
-- KEYS[1] = rate limit key, ARGV[1] = limit, ARGV[2] = window seconds
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[2])
end
if current > tonumber(ARGV[1]) then
return 0
end
return 1Atomic, one round trip, and free of the race between INCR and EXPIRE that the two-command
version has when the process dies between them.