Level 2 · 25 min
Lua Scripting
Lua scripts in Redis execute atomically — no other commands run while a script executes. This enables complex multi-step operations that must be atomic without using MULTI/EXEC transactions.
Lua Atomicity
Redis embeds LuaJIT and executes scripts synchronously in its main event-loop thread — no I/O, no yielding, no coroutines that would release the thread mid-execution. redis.call() propagates Redis errors as Lua errors (bubbled to EVAL caller). redis.pcall() catches Redis errors and returns them as a Lua table {err="..."}. KEYS and ARGV are 1-indexed arrays — accessing KEYS[0] returns nil silently, not an error; this is a common off-by-one bug. The separation of KEYS from ARGV is not enforced locally but is mandatory for Redis Cluster: the client library uses KEYS[1] to determine which cluster node to route the command to. All keys accessed by the script must hash to the same slot, or the script fails with a CROSSSLOT error. Use hash tags to guarantee co-location: {user:1001}:counter and {user:1001}:limit always hash to the same slot, regardless of the surrounding key text.
Rate Limiting with Lua
A production inventory incident: a checkout service used a non-atomic check-decrement sequence: GET stock → if > 0 then DECR stock; SETEX reservation:{id} 300 1. Under 5,000 concurrent checkout requests racing for the last unit, the gap between GET and DECR allowed 47 oversells in 800ms — each goroutine read stock=1, then all decremented. The Lua fix made the entire operation atomic: local stock = redis.call("GET", KEYS[1]); if not stock or tonumber(stock) <= 0 then return 0 end; redis.call("DECRBY", KEYS[1], 1); redis.call("SETEX", KEYS[2], 300, ARGV[1]); return 1. In load testing at 10,000 concurrent requests, zero oversells occurred. The critical property: no other command can execute between lines of a Lua script — the check and the decrement are an indivisible unit. Production insight from Redis in Action: SCRIPT LOAD returns a 40-character SHA1 hash — subsequent calls use EVALSHA with that hash, so script bytes are transmitted to Redis only once per server restart, not on every invocation; Carlson describes a fallback pattern where if EVALSHA returns a NOSCRIPT error (server restarted or SCRIPT FLUSH was called), the client transparently re-executes with EVAL to re-cache the script. For Redis Cluster deployments, all KEYS accessed inside a Lua script must reside in the same hash slot — pass all keys explicitly via the KEYS argument, not computed inside the script body, or the script will be rejected at cluster validation time.
EVALSHA and Idempotency
SCRIPT LOAD returns a 40-character SHA1 hex string. EVALSHA sha numkeys [...] executes the script by hash — script bytes are not re-transmitted on each call, saving bandwidth for large scripts. SCRIPT EXISTS sha1 [sha1...] returns 1/0 per hash. Scripts survive client reconnects but are lost on Redis restart. Recommended pattern: load scripts at application startup and cache the SHA1; on NOSCRIPT error, reload and retry. Debugging: redis.log(redis.LOG_WARNING, "debug: " .. tostring(var)) writes to the Redis log file — the only safe way to print from a script. lua-time-limit (default 5000ms): if a script runs longer than this, Redis enters a degraded state where only SCRIPT KILL and SHUTDOWN NOSAVE are accepted. SCRIPT KILL terminates a script only if it has not yet executed any write commands — otherwise the data is already mutated and SCRIPT KILL is rejected; SHUTDOWN NOSAVE is the only recovery option, losing all un-persisted data.
Code example
-- Atomic rate limiter Lua script
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2]) -- seconds
local count = redis.call('INCR', key)
if count == 1 then
redis.call('EXPIRE', key, window)
end
if count > limit then
return 0 -- rate limited
else
return 1 -- allowed
end
-- Usage from Redis CLI:
EVAL <script> 1 rate:user:alice 100 60
-- Returns 1 if under limit, 0 if over limit
-- Load once, call many times:
SCRIPT LOAD <script_text> -- returns SHA1
EVALSHA abc123def 1 rate:user:alice 100 60