Chapter 14 · Lua Scripting and Redis Functions
EVAL/EVALSHA: Atomic Server-Side Logic, KEYS/ARGV, and Script Cache Behavior
Move one small conditional AtlasMart update into Lua, make KEYS and ARGV explicit, and prove the difference between EVAL source execution and the volatile EVALSHA script cache.
Learning outcomes
AtlasMart needs to reserve one unit of inventory only when stock
is positive and record the reservation in the same Redis atomic
operation. Doing a GET, application-side branch,
then DECR creates multiple network round trips and
a race window. A small Lua script can execute the read,
decision, and writes as one atomic server-side operation—but
that does not make arbitrary application logic suitable for
Redis.
Explain EVAL and EVALSHA as Lua 5.1 server-side execution and distinguish source execution from cached execution by SHA1 digest.
Separate key-name inputs in KEYS from non-key values in ARGV and explain why declared keys matter for Cluster routing.
Observe SCRIPT LOAD/EXISTS, EVALSHA success, NOSCRIPT recovery, script-cache memory metrics, and Redis 7.4+ cache eviction behavior.
Explain why scripts are atomic yet volatile as cached application logic rather than persisted server extensions.
Repair dynamic-script generation by parameterizing values instead of producing a new script body for every request.
All Chapter 14 mandatory labs reuse the disposable Chapter 01
environment: Redis Open Source 8.10.1 from pinned
Docker image redis:8.10.1, container
atlasmart-redis-ch01, standalone topology, host
endpoint 127.0.0.1:6379, TLS disabled only
because traffic remains on loopback, default ACL user
disabled, named academy-admin and
atlasmart-app users, logical database 0, AOF with
appendfsync everysec plus RDB snapshots,
persistent /data, and no explicit
maxmemory/eviction policy. Client examples that
need Python use redis-py 8.1.0. Chapter-specific
fixtures stay under atlasmart:ch14:.
1. The practical problem: collapse a race window, not the whole service
Server-side scripting is useful when the result of one Redis
command determines the next command and that logic must be
atomic. Redis embeds Lua 5.1 and runs an
EVAL script as one server operation. Other clients
do not interleave commands while the script runs. This is
stronger than ordinary pipelining, but it also means a long
script delays every other client.
| Approach | Round trips | Atomic decision? | Operational cost |
|---|---|---|---|
| GET → client branch → DECR/HSET | Several | No, unless protected separately | Race window plus network latency |
| MULTI/EXEC only | One transaction submit | No read-before-write condition by itself | Queued atomic execution |
| WATCH + MULTI/EXEC | Several under conflict | Yes, optimistic | Retries under contention |
| Small Lua script | One request | Yes | Server is blocked for script duration |
2. KEYS are key names; ARGV are ordinary parameters
EVAL script numkeys key... arg... splits inputs
deliberately. Every Redis key that the script accesses must be
supplied in the key portion and appears in Lua as
KEYS[1], KEYS[2], and so on. Values
such as quantity, order ID, thresholds, or status text belong in
ARGV. Redis Cluster uses declared key names to
route/check the execution, so hiding key names inside
application arguments or constructing them dynamically defeats
the contract.
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 atlasmart:ch14:l1:stock atlasmart:ch14:l1:order:9001 reserve 1# Expected array: atlasmart:ch14:l1:stock, atlasmart:ch14:l1:order:9001, reserve, 1.
Do not pass a prefix in ARGV and construct
prefix .. order_id as an undeclared key. It may
appear to work in standalone Redis but violates the
declared-key discipline needed for correct Cluster routing and
tooling.
3. EVAL executes source; the script remains application-owned
The script below performs a bounded conditional update.
redis.call() invokes Redis commands inside the Lua
engine. If stock is insufficient, the script returns without
writing. If stock is sufficient, both writes occur before any
other client command can run.
SET atlasmart:ch14:l1:stock 2EVAL "local stock=tonumber(redis.call('GET',KEYS[1]) or '0'); local n=tonumber(ARGV[1]); if stock < n then return {0,stock}; end; redis.call('DECRBY',KEYS[1],n); redis.call('HSET',KEYS[2],'status','reserved','qty',n); return {1,stock-n}" 2 atlasmart:ch14:l1:stock atlasmart:ch14:l1:order:9001 1MGET atlasmart:ch14:l1:stockHGETALL atlasmart:ch14:l1:order:9001# Expected: EVAL returns [1,1]; stock is 1 and the order hash shows status=reserved, qty=1.
Atomic execution says nothing about AOF fsync timing, replica freshness, external payment systems, or exactly-once business processing. Those remain separate durability/distributed-system concerns.
4. SCRIPT LOAD + EVALSHA makes the cache observable
Redis caches Eval scripts by their SHA1 digest.
SCRIPT LOAD compiles and caches a script without
running it; SCRIPT EXISTS tests whether a digest is
currently present; EVALSHA runs only by digest. The
cache is an optimization, not persistent application state.
import redisr = redis.Redis(host="127.0.0.1", port=6379, username="academy-admin", password="AtlasMart-Admin-Lab-Only-2026", decode_responses=True)script = "local stock=tonumber(redis.call('GET',KEYS[1]) or '0'); local n=tonumber(ARGV[1]); if stock < n then return {0,stock}; end; redis.call('DECRBY',KEYS[1],n); redis.call('HSET',KEYS[2],'status','reserved','qty',n); return {1,stock-n}"sha = r.script_load(script)print("sha:", sha)print("exists:", r.script_exists(sha))r.set("atlasmart:ch14:l1:stock2", 2)print("evalsha:", r.evalsha(sha, 2, "atlasmart:ch14:l1:stock2", "atlasmart:ch14:l1:order:9002", 1))print("exists-after:", r.script_exists(sha))
5. NOSCRIPT is expected recovery behavior
The Eval script cache is volatile. A restart, failover to a node
that does not have the digest, explicit
SCRIPT FLUSH, or—on modern Redis—cache eviction can
make EVALSHA fail with NOSCRIPT. A
correct application keeps the script source and reloads/retries
safely rather than assuming the cache is durable.
import redisr = redis.Redis(host="127.0.0.1", port=6379, username="academy-admin", password="AtlasMart-Admin-Lab-Only-2026", decode_responses=True)missing = "f" * 40try: r.evalsha(missing, 0)except redis.exceptions.NoScriptError as exc: print(type(exc).__name__) # Real code would SCRIPT LOAD the known parameterized source, then retry EVALSHA once.
SCRIPT FLUSH clears the entire server script
cache, so it is inappropriate on a shared learning instance
that may contain scripts from other exercises. A fake digest
gives the same client-visible NOSCRIPT branch
without broad state deletion.
6. Redis 7.4+ can evict Eval scripts from the cache
Older guidance often described the cache as lasting until
restart or SCRIPT FLUSH. Current Redis also evicts
least-recently-used scripts loaded with EVAL/EVAL_RO
after the cache reaches its internal limit. This makes
dynamically generated script bodies especially harmful: they
waste bandwidth, compilation work, and cache memory while
forcing churn.
INFO memoryINFO stats# Observe number_of_cached_scripts / used_memory_scripts_eval where exposed and evicted_scripts on current Redis.# Do not generate thousands of unique scripts merely to force eviction in this lab.
Keep source stable and move request-specific values into
ARGV. The same script digest can serve every
AtlasMart order instead of embedding an order ID or quantity
into the Lua source.
7. RESP conversion is part of the API contract
Lua values are translated back through Redis Serialization
Protocol (RESP) rules and then into the client library's native
types. Arrays such as {1, remaining} typically
become a client array/list, integers stay integer-like, and
error replies become exceptions or error objects depending on
the client. Do not design a script return shape without testing
it through the actual client/version used by the application.
EVAL "return {1,redis.call('GET',KEYS[1]),ARGV[1]}" 1 atlasmart:ch14:l1:stock request-42# redis-cli shows an array; redis-py returns a Python list (bytes unless decode_responses=True).
8. Script cache is not Functions persistence
Eval scripts are application-owned and cached ephemerally. Redis Functions, introduced in Redis 7, are named server libraries loaded deliberately, persisted in RDB/AOF, and replicated. Chapter 14 uses both so the lifecycle difference is unmistakable rather than presenting Functions as merely another spelling of EVALSHA.
| Property | Eval script | Redis Function |
|---|---|---|
| Identity | SHA1 of source in volatile cache | Named function inside named library |
| Deployment owner | Application must retain/reload source | Server library lifecycle |
| Persistence | Script cache is not persisted | Stored with dataset; RDB/AOF |
| Replication | Script writes propagated; cache presence is not a deployment contract | Libraries are replicated with server state |
| Invocation | EVAL/EVALSHA | FCALL/FCALL_RO |
9. Hands-on lab: reservation script from source to cache
Run the context capture, initialize only the Lesson 1 keys, execute the parameterized script, load it, call it by SHA, and then exercise a safe NOSCRIPT branch. Record the digest and result shape as evidence.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin PINGdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin INFO serverdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin CONFIG GET appendonly appendfsync maxmemory maxmemory-policy lua-time-limitdocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin ACL WHOAMI# Record redis_version, persistence settings, maxmemory policy, and ACL identity before interpreting results.
UNLINK atlasmart:ch14:l1:stock atlasmart:ch14:l1:order:9001 atlasmart:ch14:l1:stock2 atlasmart:ch14:l1:order:9002SET atlasmart:ch14:l1:stock 2GET atlasmart:ch14:l1:stock
Verification checklist
- EVAL returned a two-element success/remaining result and both Redis keys match it.
- SCRIPT LOAD returned a 40-character SHA1 digest and SCRIPT EXISTS reported presence.
- EVALSHA executed the same parameterized logic without sending source again.
- The fake digest produced NOSCRIPT without clearing unrelated cached scripts.
- You recorded server version, ACL identity, persistence, and maxmemory context.
10. Cleanup and production judgment
UNLINK atlasmart:ch14:l1:stock atlasmart:ch14:l1:order:9001 atlasmart:ch14:l1:stock2 atlasmart:ch14:l1:order:9002
Choose Eval scripting when a small, bounded, application-owned atomic routine eliminates a meaningful race or round trip and you can tolerate/recover from cache misses. Keep scripts parameterized, declare every key, measure execution time, and test client reply conversion. Do not treat script-cache presence as durability, failover state, or deployment versioning.
Check your understanding
- Why must key names go in KEYS rather than ARGV?
- What does EVALSHA save compared with EVAL?
- What should a client do on NOSCRIPT?
- Why is a unique script body per order an anti-pattern?
- Does atomic Lua execution make an external payment atomic with Redis?
Review the answers
They are part of Redis key-routing and command-introspection semantics; declared keys are required for correct standalone/Cluster execution contracts.
It avoids resending/recompiling the full script when the SHA1 is already cached; it does not make the cache persistent.
Reload the known parameterized source with SCRIPT LOAD (or use the client helper) and retry the intended script safely.
It grows/churns the script cache and wastes network/compile work; values should be ARGV.
No. Redis atomicity covers server-side Redis execution, not external systems or durability/failover guarantees.
11. Summary and next step
You can now distinguish source execution, digest-based cache execution, declared keys, ordinary arguments, and cache volatility. Lesson 2 shifts from correctness of a small script to the operational constraints that make server-side atomicity dangerous when work is slow, nondeterministic, or unroutable.
Authoritative references
- Redis Open Source 8.10 release notes
- Redis programmability
- Scripting with Lua
- Redis Lua API reference
- Redis Functions introduction
- EVAL
- EVAL_RO
- EVALSHA
- EVALSHA_RO
- SCRIPT EXISTS
- SCRIPT LOAD
- SCRIPT FLUSH
- SCRIPT KILL
- FCALL
- FCALL_RO
- FUNCTION LOAD
- FUNCTION LIST
- FUNCTION STATS
- FUNCTION DUMP
- FUNCTION RESTORE
- FUNCTION DELETE
- Redis ACLs
- ACL DRYRUN
- Redis Cluster specification
- Scale with Redis Cluster
- Redis latency diagnosis
- SLOWLOG
- Redis persistence
- Redis replication
- redis-py documentation
- redis-py 8.1.0 on PyPI
- Redis licenses