Introduction
Every Redis Lua script receives two special 1-indexed arrays: KEYS and ARGV. Understanding how they work — and why all Redis keys must be declared in KEYS — is essential for writing correct, cluster-compatible scripts.
Key Concepts
- KEYS array — Redis keys the script will access; declared so Redis can route to the correct cluster node
- ARGV array — additional arguments (user IDs, thresholds, values) that are not keys
- 1-indexed — Lua arrays start at index 1, not 0;
KEYS[1]is the first key,KEYS[0]is nil - numkeys — the integer in the EVAL command that tells Redis how many tokens are KEYS vs. ARGV
- Cluster compatibility — Redis Cluster requires all accessed keys to be in the same slot; declaring them in KEYS enables this check
Real World Context
You are writing a script that reads a user's balance (one key) and a product's price (another key) and conditionally decrements the balance. Both keys must be in KEYS — if you hardcode a key name inside the script instead, Redis Cluster cannot route the command correctly.
Deep Dive
The EVAL command format:
bashEVAL script numkeys key1 key2 ... arg1 arg2 ...
Inside the script, these are available as:
KEYS[1]= key1,KEYS[2]= key2, ...ARGV[1]= arg1,ARGV[2]= arg2, ...
Example — purchase script with two keys:
bashEVAL " local balance = tonumber(redis.call('GET', KEYS[1])) or 0 local price = tonumber(redis.call('GET', KEYS[2])) or 0 if balance < price then return -1 end redis.call('DECRBY', KEYS[1], price) return balance - price " 2 user:42:balance product:99:price
Here numkeys is 2, so KEYS[1] = user:42:balance and KEYS[2] = product:99:price. There are no ARGV entries.
With both KEYS and ARGV:
bashEVAL " local current = tonumber(redis.call('GET', KEYS[1])) or 0 local threshold = tonumber(ARGV[1]) if current >= threshold then redis.call('SET', KEYS[2], ARGV[2]) end return current " 2 counter:visits alert:status 100 triggered
Here numkeys is 2: KEYS[1]=counter:visits, KEYS[2]=alert:status, ARGV[1]=100, ARGV[2]=triggered.
Common Pitfalls
- Hardcoding key names inside the script.
redis.call('GET', 'user:42:balance')bypasses the KEYS array. Redis Cluster will refuse to execute such scripts because it cannot verify key locality. - Off-by-one errors.
KEYS[0]andARGV[0]are both nil. Always start from index 1. - Miscounting numkeys. If numkeys is wrong, some KEYS entries become ARGV or vice versa, causing subtle bugs.
Best Practices
- Always pass all accessed Redis key names through KEYS, even in non-clustered deployments — it makes scripts future-proof for cluster migration.
- Use ARGV for non-key parameters: thresholds, user IDs, flag values, etc.
- Add a comment at the top of each script listing what KEYS[1..n] and ARGV[1..m] represent.
Summary
- KEYS[1..n] are Redis key names; ARGV[1..m] are non-key arguments
- Lua arrays are 1-indexed — KEYS[0] and ARGV[0] are always nil
- numkeys in EVAL tells Redis the boundary between KEYS and ARGV tokens
- All accessed key names must be in KEYS for Redis Cluster compatibility
- Hardcoding key names inside a script breaks cluster routing
Code Examples
# numkeys=2: KEYS[1]=user:42:balance, KEYS[2]=product:99:price
EVAL "
local balance = tonumber(redis.call('GET', KEYS[1])) or 0
local price = tonumber(redis.call('GET', KEYS[2])) or 0
if balance < price then return -1 end
redis.call('DECRBY', KEYS[1], price)
return balance - price
" 2 user:42:balance product:99:price-- KEYS[1] = counter key, KEYS[2] = alert key
-- ARGV[1] = threshold, ARGV[2] = alert value
local current = tonumber(redis.call('GET', KEYS[1])) or 0
local threshold = tonumber(ARGV[1])
if current >= threshold then
redis.call('SET', KEYS[2], ARGV[2])
end
return current