What Is Redis Lua Scripting?
Redis Lua scripting lets you send a block of Lua to the server and have it executed there as a single atomic unit. While the script runs, no other client's commands can interleave with it — the server finishes the whole script before it does anything else. That one property is the entire reason the feature exists, and it is why Lua is the standard answer to logic that has to read a value, decide something, and write a result without anyone else getting in between.
Why Lua Scripts Are Atomic
Valkey and Redis execute commands on a single thread. A script is treated as one command, so from every other client's point of view it either has not started or has already finished. There is no window in the middle for a race condition to occur.
A native transaction gives you the same isolation, but not the same power. MULTI queues commands before any of them run, so the queue cannot branch on a value it has not read yet. WATCH works around that by aborting and making the client retry. A script simply reads, decides and writes in one pass.
| MULTI / EXEC | Lua script | |
|---|---|---|
| Isolation | Yes | Yes |
| Branch on a value read mid-way | No — commands are queued first | Yes |
| Conflict handling | WATCH, then retry on abort | Not needed |
| Round trips | Several — watch, read, then queue and exec | One |
| Rollback on error | No | No |
Neither gives you rollback. If a script fails halfway, the writes it already made stand — atomic here means uninterrupted, not undoable.
EVAL, EVALSHA and the Script Cache
EVAL takes the script body, the number of keys, then the keys and arguments. Sending the full source on every call wastes bandwidth when the script is hot, so the server keeps a cache of every script it has seen, addressed by the SHA1 of its body.
SCRIPT LOAD puts a script in that cache and returns the digest; EVALSHA then runs it by digest alone. If the server has been restarted or the cache flushed, EVALSHA answers NOSCRIPT and the caller is expected to fall back to EVAL and try again. Handle that path explicitly: a bare evalSha against a digest the server no longer holds simply fails.
There are read-only variants too. EVAL_RO and EVALSHA_RO refuse to run any write command, which makes them safe to serve from a replica.
Writing a Script: KEYS and ARGV
Every key a script touches should arrive through the KEYS table, with everything else in ARGV. This is not a style preference. The server uses the declared keys to work out which shard the script belongs to, so a script that reaches for an undeclared key may work on a single node and fail once you move to Redis Cluster. In a cluster, all of a script's keys must also hash to the same slot.
Here is the case a transaction cannot express — check stock, and decrement only if there is enough:
-- KEYS[1] = inventory key, ARGV[1] = quantity requested
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil or stock < tonumber(ARGV[1]) then
return -1 -- not enough; change nothing
end
return redis.call('DECRBY', KEYS[1], ARGV[1]) -- returns the new level
Two clients running this concurrently cannot both succeed against the last unit, because neither can observe the other mid-script.
Lua Scripting in Java With Redisson
Redisson, the Valkey and Redis Java client, exposes scripting through RScript:
RScript script = redisson.getScript(StringCodec.INSTANCE);
Long remaining = script.eval(
RScript.Mode.READ_WRITE,
RESERVE_STOCK, // the Lua source above
RScript.ReturnType.LONG,
Collections.singletonList("inventory:42"), // KEYS
"3"); // ARGV
The mode declares intent: a READ_ONLY script contains no writes and can therefore be served by a replica, while READ_WRITE goes to the master. ReturnType saves you casting a raw reply, converting the response into the Java type you asked for. To use the script cache directly, scriptLoad returns the SHA1 digest and evalSha runs it. One upgrade note: Redisson 4.0.0 renamed the RScript.ReturnType values, so code written against 3.x needs checking against the current names.
Before you write any of this, it is worth knowing how much of it you get for free. Redisson is itself built on server-side Lua: putIfAbsent on an RMap, compareAndSet on an RBucket, acquiring an RLock and every permit taken from an RRateLimiter are all scripts that ship with the client. Reach for RScript when you need multi-key logic Redisson does not already model, or when you want to collapse a chatty sequence of commands into one round trip.
Limits and Gotchas
A slow script blocks the entire server. This is the direct cost of atomicity. Once a script passes busy-reply-threshold (5,000 ms by default, and named lua-time-limit before Redis 7.0) the server starts answering other clients with BUSY — but it does not stop the script, because killing it mid-way would break the atomicity guarantee it just made.
SCRIPT KILL only works before the first write. Once a script has modified data there is nothing safe to terminate, and the only remaining option is SHUTDOWN NOSAVE — which discards everything since the last save. Avoid unbounded loops and never call KEYS inside a script.
A long script can trigger a failover. Sentinel polls for health, and a node replying BUSY looks unavailable. If that outlasts down-after-milliseconds, Sentinel promotes a replica while the original master is still busy finishing your script.
The determinism advice you will read elsewhere is out of date. Redis once replicated scripts verbatim, which meant TIME or SRANDMEMBER could desynchronise a replica. Effects-based replication became the default in Redis 5.0 and verbatim replication was removed entirely in 7.0, so only the resulting writes are propagated and non-deterministic commands are fine. redis.replicate_commands() has been deprecated since 7.0 and now always succeeds.
Redis Lua Scripting: Frequently Asked Questions
Is a Redis Lua Script Atomic?
Yes. The server runs commands on a single thread and treats a script as one command, so no other client can observe the dataset partway through. Be careful what you read into the word, though: it means uninterrupted rather than transactional. Redis has no rollback, so anything written before an error occurred remains written.
What Is the Difference Between EVAL and EVALSHA?
EVAL sends the whole script body with every call. EVALSHA sends only the SHA1 digest of a script the server already has cached, which saves bandwidth on frequently used scripts. If the digest is not in the cache the server replies NOSCRIPT, and the client resends the full source with EVAL.
Why Do Redis Scripts Use KEYS and ARGV?
Declaring keys in KEYS tells the server which keys a script will touch, which is how it routes the script to the correct shard. A script that accesses an undeclared key can work on a single node and then fail in a cluster. Everything that is not a key belongs in ARGV.
Should I Use Lua Scripts or Redis Functions?
Redis Functions, added in Redis 7.0, let you register named functions in a library that persists on the server and replicates with it, rather than shipping a script from the client each time. Functions suit logic you want to manage as part of the deployment; EVAL scripts remain the simpler choice for logic that belongs with the application that sends it. Redisson exposes both, through RScript for scripts and RFunction for functions.