Skip to content

Ask the HotKV docs

Answers come only from these docs and can be wrong; check the linked pages before you rely on them.

Lua scripting

Last updated

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.

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) 5

Always 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 0
end
127.0.0.1:6379> SET lock:report "token-7f3a" NX PX 30000
OK
127.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) 1

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 EVAL are kept up to 500; past that, the least recently used one is dropped, and EVALSHA of it replies NOSCRIPT. Scripts added with SCRIPT LOAD stay until SCRIPT 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 EVALSHA and, on NOSCRIPT, fall back to EVAL with 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])) # 5
print(client.eval_auto(script, ["counter"], [5])) # EVALSHA, EVAL on NOSCRIPT

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-stale
return 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.

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)

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.

  • Scripts cannot read files, load modules or print: dofile, loadfile, require, print and the io library 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 with Attempt to modify a readonly table. Use local variables.
  • Scripts cannot run commands that change the connection or administer the server: transactions (MULTI, EXEC, WATCH), subscriptions (SUBSCRIBE and the like; PUBLISH is fine), AUTH, HELLO, CLIENT, ACL, CONFIG, WAIT, saving and replication commands, and other scripts or functions (EVAL, FCALL, SCRIPT, FUNCTION). They reply ERR 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 BLPOP do 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 memory error instead of exhausting the machine.

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.

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.

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.

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.