Lua Scripting
Lua scripting in Redis for atomic multi-step operations
You are an expert in writing and managing Lua scripts in Redis for atomic, server-side operations that cannot be achieved with individual commands. ## Key Points - **Always pass keys via the KEYS array.** This is mandatory for Redis Cluster compatibility, which hashes keys to determine the correct shard. - **Keep scripts short and fast.** Scripts block the entire Redis server. Target sub-millisecond execution. Move complex logic to application code when atomicity is not required. - **Use `defineCommand` (ioredis) or `register_script` (redis-py).** These cache the script SHA and fall back to `EVAL` transparently, saving bandwidth on repeated calls. - **Return structured data as JSON strings** from Lua via `cjson.encode()` when the result is complex. Redis Lua returns are limited to strings, integers, and arrays. - **Test scripts with `redis.log()`** during development. Log output appears in the Redis server log. - **Accessing keys not declared in KEYS.** The script will work on a single Redis instance but break on Cluster, where undeclared keys may live on a different shard. - **Assuming Lua number precision.** Lua 5.1 (embedded in Redis) uses double-precision floats. Integers above 2^53 lose precision. Pass large numbers as strings and handle them accordingly. - **Forgetting `tonumber()` on ARGV values.** All ARGV entries arrive as strings in Lua. Arithmetic on strings silently produces wrong results or errors.
skilldb get redis-skills/Lua ScriptingFull skill: 197 linesLua Scripting — Redis
You are an expert in writing and managing Lua scripts in Redis for atomic, server-side operations that cannot be achieved with individual commands.
Core Philosophy
Overview
Redis executes Lua scripts atomically on the server. While a script runs, no other command is processed, giving scripts the same isolation guarantees as a MULTI/EXEC transaction but with conditional logic, loops, and computed values. Lua scripting is the primary tool for implementing complex atomic operations that go beyond what pipelines and transactions offer.
Core Concepts
EVAL and EVALSHA
EVAL sends the full script text to Redis. EVALSHA sends only the SHA1 hash of a previously loaded script, saving bandwidth. ioredis supports defineCommand which handles SHA caching automatically.
KEYS and ARGV
Scripts receive two arrays: KEYS (the Redis keys the script will access) and ARGV (additional arguments). Always pass keys via the KEYS array so Redis Cluster can route the command to the correct shard.
Atomicity Guarantee
Redis is single-threaded for command execution. A Lua script runs to completion without interleaving, making it impossible for another client to observe an intermediate state.
Script Limits
Scripts must complete within the lua-time-limit (default 5 seconds). After this threshold, Redis starts accepting SCRIPT KILL and other limited commands but does not terminate the script automatically. Long-running scripts block all other operations.
Implementation Patterns
Compare-and-swap
import Redis from "ioredis";
const redis = new Redis();
// Atomic compare-and-swap: set key to newValue only if current value is oldValue
const casScript = `
local current = redis.call('GET', KEYS[1])
if current == ARGV[1] then
redis.call('SET', KEYS[1], ARGV[2])
return 1
end
return 0
`;
const swapped = await redis.eval(casScript, 1, "config:version", "v1", "v2");
// Returns 1 if swap succeeded, 0 if current value was not "v1"
Atomic transfer between two keys
const transferScript = `
local from = tonumber(redis.call('GET', KEYS[1]) or '0')
local amount = tonumber(ARGV[1])
if from < amount then
return -1 -- insufficient funds
end
redis.call('DECRBY', KEYS[1], amount)
redis.call('INCRBY', KEYS[2], amount)
return 1
`;
const result = await redis.eval(transferScript, 2, "balance:alice", "balance:bob", "50");
if (result === -1) {
console.log("Insufficient funds");
}
Using defineCommand for reusable scripts
const redis = new Redis();
// Register a custom command
redis.defineCommand("acquireLock", {
numberOfKeys: 1,
lua: `
if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then
return 1
end
return 0
`,
});
redis.defineCommand("releaseLock", {
numberOfKeys: 1,
lua: `
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0
`,
});
// Use the custom commands
const lockId = crypto.randomUUID();
const acquired = await (redis as any).acquireLock("lock:resource:42", lockId, "30000");
if (acquired) {
try {
await doWork();
} finally {
await (redis as any).releaseLock("lock:resource:42", lockId);
}
}
Sliding window counter
const slidingWindowScript = `
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
-- Remove entries outside the window
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- Count remaining entries
local count = redis.call('ZCARD', key)
if count < limit then
-- Add this request
redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
redis.call('PEXPIRE', key, window)
return 1 -- allowed
end
return 0 -- rate limited
`;
async function checkRateLimit(userId: string, windowMs: number, maxRequests: number): Promise<boolean> {
const result = await redis.eval(
slidingWindowScript, 1,
`ratelimit:${userId}`,
String(Date.now()), String(windowMs), String(maxRequests)
);
return result === 1;
}
Bulk conditional update
const bulkUpdateScript = `
local updated = 0
for i = 1, #KEYS do
local current = redis.call('GET', KEYS[i])
if current == ARGV[1] then
redis.call('SET', KEYS[i], ARGV[2])
updated = updated + 1
end
end
return updated
`;
// Update all keys from "pending" to "active"
const keys = ["job:1", "job:2", "job:3", "job:4"];
const count = await redis.eval(bulkUpdateScript, keys.length, ...keys, "pending", "active");
console.log(`Updated ${count} jobs`);
Best Practices
- Always pass keys via the KEYS array. This is mandatory for Redis Cluster compatibility, which hashes keys to determine the correct shard.
- Keep scripts short and fast. Scripts block the entire Redis server. Target sub-millisecond execution. Move complex logic to application code when atomicity is not required.
- Use
defineCommand(ioredis) orregister_script(redis-py). These cache the script SHA and fall back toEVALtransparently, saving bandwidth on repeated calls. - Return structured data as JSON strings from Lua via
cjson.encode()when the result is complex. Redis Lua returns are limited to strings, integers, and arrays. - Test scripts with
redis.log()during development. Log output appears in the Redis server log.
Common Pitfalls
- Accessing keys not declared in KEYS. The script will work on a single Redis instance but break on Cluster, where undeclared keys may live on a different shard.
- Non-deterministic operations. Scripts that call
TIME,RANDOMKEY, or usemath.random()without seeding can behave unpredictably in replication scenarios. Redis 7+ relaxed some restrictions with script flags, but be cautious. - Exceeding
lua-time-limit. A script that runs too long triggers a "BUSY" error for all other clients. There is no automatic kill; an admin must runSCRIPT KILLorSHUTDOWN NOSAVEif the script performed writes. - Assuming Lua number precision. Lua 5.1 (embedded in Redis) uses double-precision floats. Integers above 2^53 lose precision. Pass large numbers as strings and handle them accordingly.
- Forgetting
tonumber()on ARGV values. All ARGV entries arrive as strings in Lua. Arithmetic on strings silently produces wrong results or errors.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add redis-skills
Related Skills
Caching Patterns
Cache-aside, write-through, and write-behind caching strategies with Redis
Data Structures
Redis core data structures including strings, hashes, sets, sorted sets, and lists
Pub Sub
Redis Pub/Sub messaging patterns for real-time event broadcasting
Sentinel Cluster
Redis Sentinel and Cluster configurations for high availability and horizontal scaling
Streams
Redis Streams for durable event processing with consumer groups
Adversarial Code Review
Adversarial implementation review methodology that validates code completeness against requirements with fresh objectivity. Uses a coach-player dialectical loop to catch real gaps in security, logic, and data flow.