Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/redis/redis/llms.txt

Use this file to discover all available pages before exploring further.

Redis Lua scripting allows you to execute complex operations atomically on the server side. Scripts run in the same context as Redis commands, providing full access to data while maintaining atomicity and reducing network round trips.

Overview

Lua scripting in Redis provides:
  • Atomicity: Scripts execute as a single atomic operation
  • Server-Side Execution: Reduces network latency and round trips
  • Access to All Commands: Scripts can call any Redis command
  • Conditional Logic: Implement complex business logic with branching
  • Data Processing: Transform and compute data on the server

EVAL Command

Execute a Lua script on the Redis server.

Syntax

EVAL script numkeys [key [key ...]] [arg [arg ...]]
  • script: The Lua script code
  • numkeys: Number of keys (not key names) that follow
  • keys: Key names accessible via KEYS[] array
  • args: Additional arguments accessible via ARGV[] array

Basic Example

EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey "Hello"
# Returns: OK

EVAL "return redis.call('GET', KEYS[1])" 1 mykey
# Returns: "Hello"

EVALSHA Command

For better performance with frequently used scripts, use EVALSHA with preloaded script SHA1 hashes.
1

Load the script

SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# Returns: "4e6d8fc8bb01276962cce5371fa795a7763657ae"
2

Execute by SHA

EVALSHA 4e6d8fc8bb01276962cce5371fa795a7763657ae 1 mykey
# Returns: "Hello"
EVALSHA is significantly faster than EVAL for repeated script execution as it avoids retransmitting the script body.

Accessing Redis from Lua

redis.call()

Execute a Redis command and raise an error if the command fails:
local value = redis.call('GET', KEYS[1])
if not value then
    error('Key does not exist')
end
return value

redis.pcall()

Execute a Redis command and return an error object instead of raising an error:
local result = redis.pcall('GET', KEYS[1])
if type(result) == 'table' and result.err then
    return "Error: " .. result.err
end
return result

Key Differences

Featureredis.call()redis.pcall()
Error handlingRaises error, script stopsReturns error table, script continues
Use caseWhen errors should abortWhen errors need custom handling

Script Examples

Atomic Counter with Maximum

-- Increment counter only if below maximum
local current = redis.call('GET', KEYS[1])
local max = tonumber(ARGV[1])

if not current then
    current = 0
else
    current = tonumber(current)
end

if current < max then
    return redis.call('INCR', KEYS[1])
else
    return {err = 'Counter at maximum'}
end
Usage:
EVAL "local current = redis.call('GET', KEYS[1]) ... " 1 mycounter 100

Rate Limiter

-- Sliding window rate limiter
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current_time = tonumber(ARGV[3])

-- Remove old entries outside the window
redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window)

-- Count current requests
local current = redis.call('ZCARD', key)

if current < limit then
    -- Add new request
    redis.call('ZADD', key, current_time, current_time)
    redis.call('EXPIRE', key, window)
    return 1
else
    return 0
end
Usage:
EVAL "<script>" 1 "ratelimit:user:1000" 10 60 1234567890
# Allow 10 requests per 60 seconds

Conditional Update

-- Update value only if it matches expected
local key = KEYS[1]
local expected = ARGV[1]
local new_value = ARGV[2]

local current = redis.call('GET', key)

if current == expected then
    redis.call('SET', key, new_value)
    return 1
else
    return 0
end

Multi-Key Transaction

-- Transfer amount between two accounts
local from_account = KEYS[1]
local to_account = KEYS[2]
local amount = tonumber(ARGV[1])

local from_balance = tonumber(redis.call('GET', from_account) or 0)

if from_balance >= amount then
    redis.call('DECRBY', from_account, amount)
    redis.call('INCRBY', to_account, amount)
    return 1
else
    return {err = 'Insufficient balance'}
end

Data Type Conversions

Redis automatically converts between Lua and Redis data types:

Redis to Lua

Redis TypeLua TypeExample
Integernumber4242
Bulk stringstring"hello""hello"
Arraytable (array)[1, 2, 3]{1, 2, 3}
Nullbooleannilfalse
StatustableOK{ok="OK"}
ErrortableError → {err="..."}

Lua to Redis

Lua TypeRedis TypeExample
numberinteger42:42
stringbulk string"hello"$5\r\nhello
table (array)array{1,2,3}*3...
table (with ok)status{ok="OK"}+OK
table (with err)error{err="ERR"}-ERR
trueinteger 1true:1
falsenullfalse$-1

Script Management

SCRIPT LOAD

Load a script and return its SHA1 hash:
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# Returns: "4e6d8fc8bb01276962cce5371fa795a7763657ae"

SCRIPT EXISTS

Check if scripts exist in the cache:
SCRIPT EXISTS sha1 [sha1 ...]
# Returns: (integer) 1 if exists, 0 if not

SCRIPT FLUSH

Remove all scripts from the script cache:
SCRIPT FLUSH [ASYNC|SYNC]

SCRIPT KILL

Kill a running script (only if it hasn’t performed writes):
SCRIPT KILL
If a script has already performed write operations, SCRIPT KILL will return an error. You must use SHUTDOWN NOSAVE to terminate the server.

Debugging Scripts

Logging

Use redis.log() for debugging:
redis.log(redis.LOG_WARNING, 'Debug value: ' .. tostring(value))
Log levels:
  • redis.LOG_DEBUG
  • redis.LOG_VERBOSE
  • redis.LOG_NOTICE
  • redis.LOG_WARNING

redis.debug()

Log debug information during script execution:
local value = redis.call('GET', KEYS[1])
redis.debug('Current value:', value)

Atomic Script Execution Benefits

Without Scripting (Race Condition)

# Client 1
GET counter  # Returns "5"
# ... compute new value: 6 ...
SET counter 6

# Client 2 (executes between Client 1's GET and SET)
GET counter  # Returns "5"
# ... compute new value: 6 ...
SET counter 6

# Result: Increment lost! Counter is 6, should be 7

With Scripting (Atomic)

local current = redis.call('GET', KEYS[1])
if not current then current = 0 end
local new_value = tonumber(current) + 1
redis.call('SET', KEYS[1], new_value)
return new_value
No race condition possible - the entire operation is atomic.

Best Practices

1. Keep Scripts Short

Long-running scripts block other clients. Keep execution time minimal.

2. Use Script Caching

Prefer EVALSHA over EVAL for frequently used scripts:
import redis
import hashlib

r = redis.Redis()

script = "return redis.call('GET', KEYS[1])"
sha = hashlib.sha1(script.encode()).hexdigest()

# Load once
r.script_load(script)

# Execute many times
for i in range(1000):
    r.evalsha(sha, 1, f'key:{i}')

3. Declare All Keys Upfront

Always specify keys in the KEYS array for cluster compatibility:
-- Good: Keys declared
redis.call('GET', KEYS[1])
redis.call('SET', KEYS[2], value)

-- Bad: Hardcoded keys
redis.call('GET', 'hardcoded:key')  -- Won't work in cluster

4. Handle Errors Gracefully

Use redis.pcall() when errors are expected:
local result = redis.pcall('GET', KEYS[1])
if type(result) == 'table' and result.err then
    -- Handle error
    return nil
end
return result

5. Return Meaningful Values

Return structured data when helpful:
return {
    success = 1,
    old_value = old,
    new_value = new,
    timestamp = current_time
}

Limitations

Blocking: Scripts block all other Redis operations during execution. Keep them fast.
No Randomness: Scripts must be deterministic. math.random() is replaced with a pseudo-random function seeded per script execution.
Limited Globals: Scripts run in a sandboxed environment with limited Lua standard library access.

Available Lua Libraries

Redis provides these Lua libraries:
  • base: Basic Lua functions
  • table: Table manipulation
  • string: String manipulation
  • math: Mathematical functions (with deterministic random)
  • cjson: JSON encoding/decoding
  • cmsgpack: MessagePack encoding/decoding
  • struct: Binary data packing
  • bit: Bitwise operations

JSON Example

local cjson = require "cjson"

local data = {name = "Alice", age = 30}
local json = cjson.encode(data)

redis.call('SET', KEYS[1], json)

local stored = redis.call('GET', KEYS[1])
local decoded = cjson.decode(stored)
return decoded.name

Script Replication

By default, scripts are replicated as-is to replicas. The script executes on both master and replicas.

Effects Mode (RESP3)

In RESP3, you can control replication with shebangs:
#!lua flags=no-writes

-- Read-only script, won't replicate
return redis.call('GET', KEYS[1])
Available flags:
  • no-writes: Script doesn’t perform writes
  • allow-oom: Script can execute when Redis is out of memory
  • allow-stale: Script can execute on stale replicas
  • no-cluster: Script cannot run in cluster mode
  • allow-cross-slot-keys: Script can access keys from different slots

Real-World Use Cases

Session Management with Expiry

local session_key = KEYS[1]
local user_data = ARGV[1]
local ttl = tonumber(ARGV[2])

redis.call('SET', session_key, user_data, 'EX', ttl)
return redis.call('TTL', session_key)

Leaderboard Update

local leaderboard = KEYS[1]
local user_id = ARGV[1]
local score = tonumber(ARGV[2])

-- Update score
redis.call('ZADD', leaderboard, score, user_id)

-- Get user's rank
local rank = redis.call('ZREVRANK', leaderboard, user_id)

return {score = score, rank = rank}

Distributed Lock

local lock_key = KEYS[1]
local token = ARGV[1]
local ttl = tonumber(ARGV[2])

-- Acquire lock only if it doesn't exist
local acquired = redis.call('SET', lock_key, token, 'NX', 'EX', ttl)

if acquired then
    return 1
else
    return 0
end
For complex distributed locking, consider using the Redlock algorithm or dedicated libraries.