Lua scripting
A Lua script runs on the server, next to the data, as one isolated step: no
other client’s command runs while it executes. Use scripts when a write
depends on values you read first (check-and-set, conditional updates, moving
data between keys) and you want it done in one round trip without a WATCH
retry loop. Scripts written for Redis run unchanged.
For reusable server-side logic with names, consider functions, which use the same Lua environment.
Running a script
Section titled “Running a script”EVAL takes the script, the number of key arguments, the
keys, and then any other arguments. Inside the script, the keys are in the
KEYS table and the other arguments in ARGV, both indexed from 1.
redis.call() runs a command.
127.0.0.1:6379> EVAL "return redis.call('INCRBY', KEYS[1], ARGV[1])" 1 counter 5(integer) 5Always pass the keys a script touches through KEYS, not built inside the
script. Cluster routing and ACL key checks rely on knowing them in advance.
A conditional update, which deletes a lock only if it still holds the caller’s token:
if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1])else return 0end127.0.0.1:6379> SET lock:report "token-7f3a" NX PX 30000OK127.0.0.1:6379> EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:report token-7f3a(integer) 1The script cache
Section titled “The script cache”Every script you run is cached under the SHA1 digest of its source. SCRIPT LOAD adds a script without running it and returns the digest; EVALSHA runs a cached script by digest, so you send the source only once.
127.0.0.1:6379> SCRIPT LOAD "return redis.call('GET', KEYS[1])""d3c21d0c2b9ca22f82737626a27bcaf5d288f99f"127.0.0.1:6379> EVALSHA d3c21d0c2b9ca22f82737626a27bcaf5d288f99f 1 counter"5"127.0.0.1:6379> EVALSHA 0000000000000000000000000000000000000000 0(error) NOSCRIPT No matching script. Please use EVAL.| Command | What it does |
|---|---|
| SCRIPT LOAD | Cache a script and return its SHA1 |
| SCRIPT EXISTS | 1 or 0 for each digest |
SCRIPT FLUSH [ASYNC | SYNC] |
Empty the cache |
| SCRIPT KILL | Stop a script that is running too long (see below) |
| SCRIPT DEBUG | Only NO is accepted; there is no script debugger |
Keep these rules in mind:
- Scripts cached by
EVALare kept up to 500; past that, the least recently used one is dropped, andEVALSHAof it repliesNOSCRIPT. Scripts added withSCRIPT LOADstay untilSCRIPT FLUSH. - The cache is held in memory on each server. It is not saved, so after a restart, or on another node of a cluster, a script you loaded earlier is gone.
- The usual pattern is to call
EVALSHAand, onNOSCRIPT, fall back toEVALwith the source, which also caches it again. Several SDKs do this for you.
import hotkv
client = hotkv.HotKV(host="localhost", port=6379)
script = "return redis.call('INCRBY', KEYS[1], ARGV[1])"print(client.eval(script, ["counter"], [5])) # 5print(client.eval_auto(script, ["counter"], [5])) # EVALSHA, EVAL on NOSCRIPTimport { HotKV } from "@hotkv/client";
const client = new HotKV({ url: "hotkv://127.0.0.1:6379" });
const script = "return redis.call('INCRBY', KEYS[1], ARGV[1])";console.log(await client.eval(script, ["counter"], [5]));console.log(await client.evalShaOrLoad(script, ["counter"], [5])); // EVALSHA, EVAL on NOSCRIPTimport java.util.List;
String script = "return redis.call('INCRBY', KEYS[1], ARGV[1])";Object result = client.eval(script, List.of("counter"), List.of("5"));
String sha = client.scriptLoad(script);Object again = client.evalSha(sha, List.of("counter"), List.of("5"));Read-only scripts and flags
Section titled “Read-only scripts and flags”EVAL_RO and EVALSHA_RO run a script that may not write. A write inside it fails, and they can run on a read-only replica.
A script can also declare flags on a first line that starts with #!lua:
#!lua flags=no-writes,allow-stalereturn redis.call('GET', KEYS[1])| Flag | Effect |
|---|---|
no-writes |
The script may not write; it can run with EVAL_RO and on replicas |
allow-oom |
The script may run when the server is over maxmemory |
allow-stale |
Accepted for compatibility; it changes nothing, since HotKV replicas serve reads either way |
no-cluster |
The script refuses to run on a cluster node |
allow-cross-slot-keys |
In a cluster, the script may touch keys of several slots on the node |
A script with a #!lua line and no no-writes flag is refused up front when
the server cannot take writes (a read-only replica, over maxmemory without
allow-oom, or a failing disk), instead of failing part way.
What a script can use
Section titled “What a script can use”Scripts get the Lua 5.1 functions and behavior Redis scripts rely on,
including Redis’s number handling (10 / 2 returns 5 to the client) and
the Lua 5.1 compatibility functions such as unpack, table.getn and
loadstring. The available libraries are:
| Library | Contents |
|---|---|
| Base functions | type, tostring, tonumber, pairs, ipairs, select, error, pcall, assert, setmetatable, getmetatable, … |
string, table, math |
The standard libraries |
coroutine |
Coroutines |
os |
os.clock only |
cjson |
JSON encoding and decoding |
cmsgpack |
MessagePack encoding and decoding |
struct |
Binary packing and unpacking |
bit |
Bitwise operations |
The redis table provides:
| Function | Use |
|---|---|
redis.call(command, ...) |
Run a command; an error reply raises a Lua error that ends the script |
redis.pcall(command, ...) |
Run a command; an error reply comes back as a table {err = "..."} |
redis.error_reply(text), redis.status_reply(text) |
Build an error or status reply to return |
redis.sha1hex(text) |
The SHA1 of a string |
redis.log(level, message) |
Write to the server log (redis.LOG_DEBUG, LOG_VERBOSE, LOG_NOTICE, LOG_WARNING) |
redis.setresp(2 or 3) |
Choose the protocol for the replies redis.call() returns to the script |
redis.set_repl(...) |
Choose where the script’s writes are propagated (redis.REPL_ALL, REPL_AOF, REPL_REPLICA, REPL_NONE) |
redis.acl_check_cmd(command, ...) |
Whether the calling user may run a command |
redis.REDIS_VERSION, redis.REDIS_VERSION_NUM |
The Redis version HotKV is compatible with (8.4.0) |
Converting values
Section titled “Converting values”A script’s return value becomes the reply: a Lua number becomes an integer
(the fraction is dropped, so return floats as strings), a string a bulk
string, a table an array (up to its first nil), true the integer 1,
false and nil a null reply, and {ok = "..."} / {err = "..."} a
status or error reply. Command replies come into the script the opposite way.
Sandbox rules
Section titled “Sandbox rules”- Scripts cannot read files, load modules or print:
dofile,loadfile,require,printand theiolibrary are not there. - Scripts cannot create or change global variables, or change the standard
tables. Reading a global that does not exist is an error
(
Script attempted to access nonexistent global variable), and assigning one fails withAttempt to modify a readonly table. Uselocalvariables. - Scripts cannot run commands that change the connection or administer the
server: transactions (
MULTI,EXEC,WATCH), subscriptions (SUBSCRIBEand the like;PUBLISHis fine),AUTH,HELLO,CLIENT,ACL,CONFIG,WAIT, saving and replication commands, and other scripts or functions (EVAL,FCALL,SCRIPT,FUNCTION). They replyERR This Redis command is not allowed from script. - A script runs with the permissions of the user who called it: every command and key it touches is checked against that user’s ACL rules.
- Blocking commands such as
BLPOPdo not wait inside a script; they answer at once as if their timeout had passed. - A script’s memory is limited: a script that uses too much gets Lua’s
not enough memoryerror instead of exhausting the machine.
Long-running scripts
Section titled “Long-running scripts”While a script runs, nothing else can change the data it may touch, so keep
scripts short. When a script has run longer than busy-reply-threshold
(5000 ms by default; lua-time-limit is an alias), other clients whose
commands have to wait for it get:
(error) BUSY HotKV is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.The script itself keeps running. SCRIPT KILL stops
it, as long as it has not written anything yet; once it has written, stopping
it would leave a partial change, so SCRIPT KILL replies UNKILLABLE and
only SHUTDOWN NOSAVE can end it. With no script running, SCRIPT KILL
replies NOTBUSY.
Scripts in a cluster
Section titled “Scripts in a cluster”The keys a script declares in KEYS must all hash to one slot served by the
node, like a multi-key command; redis-cli -c and the SDKs’ cluster clients
route EVAL by its first key. Every key the script actually touches must be
on that node, or the command fails inside the script with
Script attempted to access a non local key in a cluster node. A script with
a #!lua line must keep to one slot unless it declares
allow-cross-slot-keys; a script without that line may touch keys of other
slots on the same node. Remember that the script cache is per node: load
scripts on every node, or rely on the EVALSHA-then-EVAL fallback. See
Scripts and functions in a cluster
for the errors and examples.
Replication and persistence
Section titled “Replication and persistence”The writes a script makes are sent to replicas and the append-only file as the commands it ran, wrapped as one unit, so replicas never re-run the script and get the same result even when it used the time or random values.
Compatibility
Section titled “Compatibility”EVAL, EVALSHA, EVAL_RO, EVALSHA_RO, SCRIPT and the redis.* API
behave as in Redis 7, including script flags and the 500-script limit on
EVAL-cached scripts from Redis 7.4. There is no Lua debugger: SCRIPT DEBUG YES and SYNC reply ERR the Lua debugger is not supported by this server.
Was this page helpful?
Thanks for the feedback.