Skip to content
Navigation

Type at least two characters. Search covers page titles, headings, tags and database names.

↑ ↓ to navigateEnter to openEsc to close0 pages
Redis and Valkeyintermediate

Redis Lua Scripts

Running atomic server-side logic with EVAL and functions, the determinism rules, and why a long script is an outage.

2 min readIntermediateUpdated Edit this page

A Lua script runs atomically on the server: no other command executes while it does. That makes it the cleanest way to express read-modify-write logic in Redis.

EVAL "return redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2])" 1 mykey myvalue 60

Keys must be declared

Pass every key the script touches through KEYS, never hard-coded inside the script body:

EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then
        return redis.call('DEL', KEYS[1])
      else
        return 0
      end" 1 lock:orders my-token

This is not a style rule. Redis Cluster routes a script by its declared keys; a script that accesses an undeclared key may run on the wrong node and behave incorrectly. All declared keys must also hash to the same slot.

Loading and reuse

SCRIPT LOAD "return redis.call('INCR', KEYS[1])"
> "e0e1f9fabfc9d4800c877a703b823ac0578ff831"
 
EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff831 1 counter:page-views

EVALSHA sends the hash rather than the body. Clients should fall back to EVAL on a NOSCRIPT error, because the script cache is cleared by restarts and by SCRIPT FLUSH.

Redis 7 adds Functions (FUNCTION LOAD), which persist across restarts and replicate as part of the dataset — a better fit than scripts for logic that is part of the application's contract.

Determinism

Scripts are replicated by effect in modern versions, but determinism still matters for correctness and for the constraints Redis enforces:

  • Use redis.call('TIME') rather than Lua's clock if you need time, and be aware it makes the script non-deterministic in older replication modes.
  • Do not iterate over unordered collections and write based on the order — SMEMBERS has no guaranteed order.
  • Never generate random values inside a script without seeding from an argument.

Scripts block everything

A worthwhile use: rate limiting

-- KEYS[1] = rate limit key, ARGV[1] = limit, ARGV[2] = window seconds
local current = redis.call('INCR', KEYS[1])
if current == 1 then
  redis.call('EXPIRE', KEYS[1], ARGV[2])
end
if current > tonumber(ARGV[1]) then
  return 0
end
return 1

Atomic, one round trip, and free of the race between INCR and EXPIRE that the two-command version has when the process dies between them.