# Lua scripting

> Run Lua scripts on the server with EVAL and EVALSHA, manage the script cache, and know the sandbox and time limits.

URL: https://docs.hotkv.com/develop/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](https://docs.hotkv.com/develop/functions/index.md), which use the same Lua environment.

## Running a script

[EVAL](https://docs.hotkv.com/commands/eval/index.md) 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:

```lua
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
```

## The script cache

Every script you run is cached under the SHA1 digest of its source.
[SCRIPT LOAD](https://docs.hotkv.com/commands/script-load/index.md) adds a script without running it and
returns the digest; [EVALSHA](https://docs.hotkv.com/commands/evalsha/index.md) 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](https://docs.hotkv.com/commands/script-load/index.md) | Cache a script and return its SHA1 |
| [SCRIPT EXISTS](https://docs.hotkv.com/commands/script-exists/index.md) | `1` or `0` for each digest |
| [SCRIPT FLUSH](https://docs.hotkv.com/commands/script-flush/index.md) `[ASYNC \| SYNC]` | Empty the cache |
| [SCRIPT KILL](https://docs.hotkv.com/commands/script-kill/index.md) | Stop a script that is running too long (see below) |
| [SCRIPT DEBUG](https://docs.hotkv.com/commands/script-debug/index.md) | 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.

**Python**

```python
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
```
**Node.js**

```ts
import { 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 NOSCRIPT
```
**Java**

```java
import 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

[EVAL_RO](https://docs.hotkv.com/commands/eval_ro/index.md) and [EVALSHA_RO](https://docs.hotkv.com/commands/evalsha_ro/index.md) 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
#!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.

## 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

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

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

## 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](https://docs.hotkv.com/commands/script-kill/index.md) 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

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](https://docs.hotkv.com/clustering/keys-and-commands/index.md#scripts-and-functions)
for the errors and examples.

## 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

`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`.