Transactions and Lua Scripts
Introduction
Redis commands are atomic individually, but real-world operations often require multiple steps. This chapter covers two mechanisms for grouping commands: transactions queue a batch of operations and execute them atomically, while Lua scripts run server-side logic with the full power of a programming language. Both let you reduce network round trips and avoid race conditions, though they differ in flexibility and error-handling behavior.
Prerequisites
- Comfort with basic Redis commands and data types
- Familiarity with the concept of atomicity and concurrency
- No prior Lua experience is required; syntax is explained as it appears
Transactions
A Redis transaction groups multiple commands so they execute sequentially without interruption from other clients. It is not an ACID transaction in the relational database sense—there is no rollback—but the commands do execute atomically with respect to other clients.
MULTI, EXEC, and DISCARD
# Start a transaction
MULTI
# Queue commands (they are not executed yet)
SET balance:alice 900
INCRBY balance:bob 100
# Execute the queued commands atomically
EXECBetween MULTI and EXEC, Redis buffers your commands. Other clients cannot see the intermediate state. When EXEC arrives, Redis runs all queued commands in order.
If you change your mind before EXEC:
MULTI
SET temp:flag "processing"
INCR temp:counter
DISCARD
# The transaction is canceled; nothing is executedWhat Happens When a Command Fails?
Redis distinguishes two failure modes inside a transaction:
- Command queueing failure: You send a syntactically invalid command or use it against the wrong data type. Redis rejects it immediately during
MULTI, andEXECis aborted. - Runtime failure: A command is valid but fails during execution (for example,
INCRon a String that is not numeric). Redis continues executing the remaining commands in the transaction.
Warning
No Rollback
If the third command in a five-command transaction fails, the first two have already executed and the last two will still execute. Your application must handle partial failure explicitly, usually by designing idempotent operations or compensating later.
Optimistic Locking with WATCH
WATCH implements optimistic locking. You monitor one or more keys; if any of them change before your EXEC, the transaction is aborted and returns (nil).
# Watch Alice's balance
WATCH balance:alice
# Read the current value
GET balance:alice
# "1000"
# Start the transaction
MULTI
DECRBY balance:alice 100
INCRBY balance:bob 100
EXECIf another client modifies balance:alice after your WATCH but before your EXEC, your EXEC returns (nil) and you must retry the entire read-modify-write sequence.
UNWATCH clears the watched keys without executing a transaction. It is called automatically after EXEC or DISCARD.
Lua Scripts
When transactions are not expressive enough, Lua scripts give you variables, conditionals, loops, and the ability to read intermediate results before deciding what to write.
EVAL
EVAL sends a Lua script to the Redis server. The script executes atomically: no other command runs while it is active.
# Atomically transfer credits if sufficient balance exists
EVAL "local bal = redis.call('GET', KEYS[1]); if tonumber(bal) >= tonumber(ARGV[1]) then redis.call('DECRBY', KEYS[1], ARGV[1]); redis.call('INCRBY', KEYS[2], ARGV[1]); return 1; else return 0; end;" 2 balance:alice balance:bob 100KEYS[1]isbalance:aliceKEYS[2]isbalance:bobARGV[1]is100- The number
2tells Redis how many arguments are keys (needed for cluster routing)
Tip
KEYS vs ARGV
Always pass key names through KEYS so that Redis Cluster can route the script to the correct node. Pass literal values through ARGV. Failing to do this breaks cluster compatibility.
redis.call vs redis.pcall
redis.call()executes a Redis command. If the command errors, the Lua error propagates and the script aborts.redis.pcall()executes a command but traps errors, returning them as tables you can inspect.
local result = redis.pcall('INCR', KEYS[1])
if type(result) == 'table' and result.err then
return {err = "Increment failed: " .. result.err}
else
return result
endScript Caching with EVALSHA
Sending the full script body on every request wastes bandwidth. Redis caches scripts by their SHA1 digest. After the first EVAL, use EVALSHA with the digest.
# Load the script without executing it
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# "a1b2c3d4..." (SHA1 digest)
# Execute by SHA1
EVALSHA a1b2c3d4... 1 mykeyIf the server restarts or script eviction occurs, EVALSHA returns a NOSCRIPT error. Client libraries typically fall back to EVAL automatically.
# Check if a script is cached
SCRIPT EXISTS a1b2c3d4...
# Flush all cached scripts (use with caution)
SCRIPT FLUSHA Practical Example: Rate Limiter
This Lua script implements a sliding-window rate limiter atomically:
The script returns 1 if the request is allowed and 0 if the user has exceeded the limit. Because it runs on the server, there is no race window between reading the count and adding the new entry.
When to Use Transactions vs Lua Scripts
| Situation | Use |
|---|---|
| Simple batch of independent commands | MULTI / EXEC |
| Read-modify-write with optimistic locking | WATCH + MULTI / EXEC |
| Conditional logic, loops, or computed values | Lua script |
| Need to return intermediate results to the client | Lua script |
| Complex atomic operation that would otherwise need multiple round trips | Lua script |
FAQ
Can I call blocking commands like BLPOP inside a Lua script?
No. Blocking commands, commands with non-deterministic output (like RANDOMKEY), and commands that modify the replication state are forbidden inside Lua scripts to preserve consistency.
Does Lua scripting work on Redis Cluster?
Yes, but all keys accessed by the script must hash to the same slot. Use hash tags ({user:42}:data and {user:42}:meta) to guarantee locality. If keys span multiple slots, Redis rejects the script.
What happens if a Lua script runs for too long?
Redis logs a slow-script warning and allows other clients to run SCRIPT KILL (if the script has not yet performed any writes). A script that has already written data cannot be killed safely; you must wait or restart the server. Keep scripts short.
Is there a size limit for Lua scripts?
Scripts are limited by the Lua interpreter memory inside Redis (typically 5 MB of compiled bytecode). In practice, if your script is longer than a few hundred lines, you should reconsider whether it belongs in Redis or in your application server.
Why does my transaction return an array of results?
EXEC returns the results of every queued command in order. For example, if you queue SET, INCR, and GET, the return value is ["OK", 1, "value"].
Can I nest MULTI inside another MULTI?
No. Redis rejects a second MULTI while already inside a transaction and returns an error.