Why Lua scripts execute atomically, KEYS and ARGV conventions, redis.call() vs redis.pcall(), SCRIPT LOAD and EVALSHA for script caching, atomic rate limiters and conditional operations impossible without Lua.
A-2 — Lua Scripting: EVAL, EVALSHA, and Atomic Compound Operations
Who this module is for: You have reached the limits of MULTI/EXEC and WATCH — you need to read a value, make a decision based on it, and write conditionally, all as a single atomic operation. Lua scripts run atomically on the Redis server, executing arbitrary logic without any other client interleaving. This module covers the EVAL model, the KEYS/ARGV convention, script caching, error handling, and the patterns that are impossible to implement correctly without Lua.
Why Lua Scripts Are Atomic
Redis's single-threaded event loop executes commands one at a time. A Lua script is executed as if it were a single command — it runs to completion before any other client's command executes. No other client can see intermediate state or interleave their commands during script execution.
This is stronger than MULTI/EXEC:
- MULTI/EXEC queues commands and sends them together, but does not provide read-then-decide-then-write atomicity (you cannot use the result of a read to conditionally control what you write)
- Lua executes arbitrary code server-side — you can read, branch, loop, and write all within the atomic boundary
The price: While a Lua script runs, Redis processes no other commands. Long-running scripts block all clients. Scripts must be fast (< 1ms ideally, < 5ms acceptable).
EVAL: Running a Script
EVAL script numkeys key [key ...] arg [arg ...]
script— the Lua script as a stringnumkeys— the number ofkeyarguments (required for Cluster routing)key [key ...]— key names accessible in the script asKEYS[1],KEYS[2], etc.arg [arg ...]— additional arguments accessible asARGV[1],ARGV[2], etc.
127.0.0.1:6379> EVAL "return redis.call('GET', KEYS[1])" 1 mykey
"myvalue"
127.0.0.1:6379> EVAL "return ARGV[1]" 0 "hello"
"hello"
The KEYS and ARGV Convention
KEYS — all Redis key names the script accesses. Required for Redis Cluster: the cluster client routes the command based on KEYS[1]. If your script accesses keys on different slots, it will fail in Cluster.
ARGV — all non-key parameters: values, thresholds, configuration.
The convention is enforced by policy, not the interpreter. You can technically access any key by hardcoding the name in the script, but this breaks Cluster routing. Always pass key names via KEYS.
-- Correct: keys via KEYS, values via ARGV
local current = tonumber(redis.call('GET', KEYS[1]))
local limit = tonumber(ARGV[1])
if current >= limit then
return 0
end
redis.call('INCR', KEYS[1])
return 1
redis.call vs redis.pcall
redis.call('SET', KEYS[1], ARGV[1]) -- raises error on failure; script aborts
redis.pcall('SET', KEYS[1], ARGV[1]) -- returns error as a table; script continues
redis.call propagates errors — if the Redis command fails (type mismatch, wrong arg count), the script aborts and Redis returns an error to the client.
redis.pcall catches errors and returns them as a Lua table {err = "error message"}. Use when you want to handle errors within the script:
local result = redis.pcall('INCR', KEYS[1])
if result.err then
-- Key is not a string (type error), handle gracefully
return -1
end
return result
Return Types
Lua → Redis type conversion:
| Lua | Redis reply |
|---|---|
| integer | Integer reply |
| string | Bulk string reply |
| table (array) | Multi-bulk reply |
{ok = "OK"} | Simple string reply (+OK) |
{err = "ERR msg"} | Error reply |
| false or nil | Nil bulk reply |
return 42 → (integer) 42
return "hello" → "hello"
return {1, 2, 3} → 1) (integer) 1 \n 2) (integer) 2 \n 3) (integer) 3
return {ok = "OK"} → +OK
return false → (nil)
Important: Lua numbers are always floats. When returning integers to Redis, use math.floor() or tonumber() for explicit integer conversion. return 3.14 → (integer) 3 (Redis truncates floats).
SCRIPT LOAD and EVALSHA
Sending the full script text on every call is wasteful for large scripts. SCRIPT LOAD uploads the script to Redis once and returns its SHA1 digest. EVALSHA then calls the script by SHA:
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
→ "2067d915024a3e1657c4169c84f809f8ec75b9a7"
EVALSHA 2067d915024a3e1657c4169c84f809f8ec75b9a7 1 mykey
→ "myvalue"
// Load script once at application startup
const sha = await redis.script('LOAD', scriptText);
// Call by SHA on every invocation (no retransmission of script body)
const result = await redis.evalsha(sha, 1, key, ...args);
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterSee it in production
Discussion
0Join the discussion