skip to content

Lua Scripting: EVAL & EVALSHA

You will learn how a Lua script executes atomically on the server — reading, deciding, and writing in one step no other client can interleave — and why a slow script freezes the whole event loop. Interviewers ask for a Lua rate limiter or lock release because it proves you can express atomicity beyond MULTI.

part ofRedisoverview, primer and where to startread it →
on this pageshow

questions

5

When you run a Lua script on Redis with EVAL, you pass a key count followed by keys and then other arguments, which the script sees as KEYS and ARGV. Why does Redis insist that key names be declared separately instead of just hardcoding them in the script body?

level: juniorimportance: must knowfreq 52%

answer

  1. numkeys splits KEYS from ARGV
  2. Redis can't parse Lua to find keys
  3. Cluster slot routing + ACL checks need them upfront
  4. All KEYS must share one slot — use hash tags {…}
  5. 1-indexed, always strings → tonumber()

basics

~20 s

Redis needs to know which keys a script touches before running it, so it can route the command to the right cluster node, enforce ACL key permissions and cross-slot rules. Keys go in KEYS via the numkeys count; everything else goes in ARGV.

solid answer

~50 s

`EVAL script numkeys key1 … arg1 …` splits the trailing arguments: the first `numkeys` of them become the `KEYS` table, the rest become `ARGV`. Both are 1-indexed Lua tables of strings. Redis cannot parse arbitrary Lua to discover which keys you will touch, so the declaration is the only way it knows. That knowledge is needed **before** execution: - **Cluster routing** — the client (and the server's cross-slot check) computes hash slots from `KEYS`. All declared keys must map to one slot, usually arranged with hash tags like `{user:42}:profile`. Keys smuggled in through `ARGV` are invisible to routing, so the command can land on the wrong node. - **ACLs** — key patterns granted to a user are checked against the declared keys. So build key names from `KEYS`, and use `ARGV` only for values, counts, TTLs. Never construct a key by concatenating something from `ARGV` unless you also declared it.

code

text · 10 lines
text
# correct: key declared, routable and ACL-checkable
> EVAL "return redis.call('GET', KEYS[1])" 1 session:42

# wrong: the key is built inside the script from ARGV
> EVAL "return redis.call('GET', 'session:' .. ARGV[1])" 0 42
# works on a standalone server, breaks under Cluster and ACLs

# two keys, same slot via a hash tag
> EVAL "redis.call('SET', KEYS[1], ARGV[1]); return redis.call('GET', KEYS[2])" \
       2 {user:42}:name {user:42}:email  "amy"

go deeper

for a junior

Recall the command shape, that numkeys splits KEYS from ARGV, that both are 1-indexed string tables, and that keys must be declared.

for a middle

Explain why declaration is required — cluster slot routing and ACL key checks happen before execution — and that Redis cannot infer keys from Lua.

for a senior

Point out that violating the rule works on standalone and fails on Cluster or when ACLs land, and prescribe hash tags for multi-key scripts.

for a principal

Frame key declaration as the contract that keeps data-tier logic routable and governable, and use it to argue whether a multi-key atomic operation belongs in one shard at all.

## The command shape ``` EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue ``` The `1` is `numkeys`. It tells Redis that exactly one of the following arguments is a key name. Redis then hands the script two global tables: - `KEYS` — the declared key names, `KEYS[1] … KEYS[numkeys]`; - `ARGV` — everything after them. Both are Lua tables indexed **from 1** (Lua convention, not 0), and every element is a **string**, even when you passed a number — `ARGV[1]` for `10` is `"10"`, so arithmetic needs `tonumber(ARGV[1])`. ## Why the declaration is mandatory Redis executes the script body; it does not analyse it. There is no way for the server to look at arbitrary Lua — with loops, string concatenation, conditionals — and determine which keys it will end up touching. The only reliable source of that information is the caller. Several mechanisms depend on having it *before* the script runs. **Cluster routing.** In Redis Cluster the keyspace is split into 16384 hash slots, and each slot lives on one shard. A client decides which node to send a command to by hashing its keys. For `EVAL` the keys are precisely the declared ones. If you hide a key in `ARGV`, the client may route the script to a node that does not own that key, and the operation fails or, worse, the script's own `redis.call` gets a `CROSSSLOT`/`MOVED`-style rejection mid-execution. Redis also enforces that all declared keys hash to the **same slot** — a script may not span slots. The standard remedy is hash tags: naming keys `{user:42}:profile` and `{user:42}:sessions` forces both into the slot computed from `user:42`. **Access control.** ACL rules grant users patterns of keys (`~cache:*`). The server checks the declared keys against those patterns. Undeclared keys would let a script bypass the grant. **Correct behaviour of tooling.** Proxies, connection routers and observability tooling all reason about the keys a command touches; a script that lies about its keys is opaque to all of them. ## The rule in practice > Every key the script reads or writes must arrive through `KEYS`. That means never doing: ```lua -- WRONG: the key is invisible to routing and ACLs return redis.call('GET', 'session:' .. ARGV[1]) ``` Instead the caller builds the full key name and passes it as a key: ```lua -- RIGHT return redis.call('GET', KEYS[1]) ``` A legitimate middle ground exists for deriving a *related* key with the same hash tag (e.g. a suffix), but the safe habit — and what an interviewer wants to hear — is that key names are computed by the client and declared. On a standalone (non-cluster) server, a script that cheats often *works*, which is exactly why the habit matters: the code passes tests and then breaks the day the deployment moves to Cluster, or the day ACLs are turned on. ## Getting numkeys wrong - **Too small**: a key you intended lands in `ARGV`, so `KEYS[2]` is `nil` and `redis.call` fails with an error about a nil argument, or the script silently operates on the wrong thing. - **Too large**: an argument is treated as a key, poisoning routing and ACL checks. - **Negative or non-numeric**: Redis rejects the command outright. Redis cannot detect a semantically wrong `numkeys`, only a syntactically impossible one — so this is a class of bug that only tests and code review catch. ## Related detail Redis Functions (7.0+) express the same idea with plain parameters: the registered callback receives `(keys, args)` instead of the `KEYS`/`ARGV` globals, and `FCALL fname numkeys …` carries the same declaration. The discipline is identical; only the syntax differs. Also note that neither table is a way to pass structured data: everything is a flat list of strings. Complex input is usually serialized (JSON in one `ARGV` slot) — but keys must still be declared individually.

  • Your script needs two keys and the cluster rejects it with a CROSSSLOT error. What do you do?
    All keys a script touches must live in the same hash slot. Rename them so they share a hash tag — the substring between the first { and } is what gets hashed, so {user:42}:profile and {user:42}:sessions land on the same shard. If the keys genuinely belong to different entities and cannot share a tag, the operation cannot be one atomic script; split it into separate calls and handle the lack of atomicity in the application.
  • Is ARGV[1] a number when you pass 10?
    No. Every element of KEYS and ARGV is a string, so ARGV[1] is "10". Arithmetic or comparisons need tonumber(ARGV[1]), and values returned to Redis go the other way — Lua numbers are converted to integers, truncating any fractional part, so return a string if you need a float.

It is a customs declaration: the server needs the manifest before the box travels, because the routing and the permission check happen at the border, not after you open it.

saying these in an interview costs you the question

  • Building key names inside the script from ARGV and calling it equivalent
  • Thinking Redis inspects the script to discover its keys
  • Indexing KEYS[0] or ARGV[0]
  • Doing arithmetic on ARGV values without tonumber
  • Believing a script can atomically span multiple hash slots in Cluster

context

open as a page

A Lua script sent to Redis with EVAL is described as executing atomically. What exactly does that guarantee, what does it cost the rest of the server, and what can an operator do about a script that will not finish?

level: middleimportance: must knowfreq 54%

basics

~20 s

The script runs to completion with no other client command interleaved, so read-modify-write logic is safe. The cost: it occupies the single command-processing thread, so every other client waits. After busy-reply-threshold Redis replies BUSY and accepts SCRIPT KILL — but only if the script has not written.

open as a page

Redis lets you run a Lua script either by sending its full source with EVAL or by sending a SHA1 digest with EVALSHA. Why does EVALSHA exist, what error must a client be ready for, and how do SCRIPT LOAD and SCRIPT EXISTS fit in?

level: middleimportance: must knowfreq 48%

basics

~20 s

EVALSHA sends only the script's SHA1 instead of its body, saving bandwidth. Redis caches every script it has seen, but that cache is ephemeral, so EVALSHA can fail with NOSCRIPT — the client must then SCRIPT LOAD or fall back to EVAL and retry.

open as a page

Inside a Redis Lua script you can invoke commands with either redis.call or redis.pcall. What is the difference in how they handle a command that fails, and which should you reach for?

level: seniorimportance: should knowfreq 30%

basics

~20 s

redis.call raises a Lua error on command failure, aborting the script and returning the error to the client. redis.pcall returns the error as a Lua table instead, so the script keeps running and must inspect it. Default to redis.call; use pcall only when you will handle the failure.

open as a page

Redis Lua scripts historically had strict determinism rules — no random values, no reliance on the clock, sorted output for unordered commands. Why did those rules exist, and what changed once Redis started replicating script effects instead of the script itself?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Originally Redis shipped the script itself to replicas and the AOF, so both sides had to produce identical results — any randomness or clock use would diverge them. Modern Redis replicates the script's resulting write commands instead, so nondeterminism is safe, though determinism still aids reasoning.

open as a page