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

PracticeReason
Always pass keys via KEYS[]Required for Redis Cluster key routing
Keep scripts shortLong scripts block Redis (single-threaded)
Use EVALSHA in productionAvoids resending script bytes every call
No I/O or sleep in scriptsBlocks the event loop
Use tonumber() / tostring()Redis returns bulk strings; cast as needed