Scripting Commands

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
Free Download

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 counter

2. 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 100

KEYS Array

  • KEYS[1] - First key argument
  • KEYS[2] - Second key argument
  • Used for all Redis key names
  • Required for cluster compatibility

ARGV Array

  • ARGV[1] - First non-key argument
  • ARGV[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 mykey

Client Libraries Handle This

Most Redis clients (ioredis, redis-py, etc.) automatically use EVALSHA and fall back to EVAL on NOSCRIPT error. You usually don't need to manage this manually.

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 raise

5. 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 60

Atomic 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-123

Leaderboard 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 150

6. 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

Redis has a 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_WARNING

Script 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 KILL

8. 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 cluster

Hash Tags

Use hash 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

CommandPurpose
EVAL script numkeys ...Run Lua script
EVALSHA sha numkeys ...Run cached script by SHA
SCRIPT LOAD scriptCache script, return SHA
SCRIPT EXISTS sha ...Check if scripts cached
SCRIPT FLUSHClear script cache
SCRIPT KILLKill 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.

Download Redimo - It's Free

Continue Learning

Free Download

Stop Fighting with CLI.
Visualize Your Data.

Redimo makes every Valkey and Redis command easier. Visual data, inline editing, Safe Mode for production.