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?
answer
- numkeys splits KEYS from ARGV
- Redis can't parse Lua to find keys
- Cluster slot routing + ACL checks need them upfront
- All KEYS must share one slot — use hash tags {…}
- 1-indexed, always strings → tonumber()
basics
~20 sRedis 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# 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
Recall the command shape, that numkeys splits KEYS from ARGV, that both are 1-indexed string tables, and that keys must be declared.
Explain why declaration is required — cluster slot routing and ACL key checks happen before execution — and that Redis cannot infer keys from Lua.
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.
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