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.

Advanced170–230 minutesLua scripting and script cacheRedis Open Source 8.10.1redis-py 8.1.0 where usedFree/local-firstLast reviewed: September 6, 2026

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.

01

Explain EVAL and EVALSHA as Lua 5.1 server-side execution and distinguish source execution from cached execution by SHA1 digest.

02

Separate key-name inputs in KEYS from non-key values in ARGV and explain why declared keys matter for Cluster routing.

03

Observe SCRIPT LOAD/EXISTS, EVALSHA success, NOSCRIPT recovery, script-cache memory metrics, and Redis 7.4+ cache eviction behavior.

04

Explain why scripts are atomic yet volatile as cached application logic rather than persisted server extensions.

05

Repair dynamic-script generation by parameterizing values instead of producing a new script body for every request.

Exact lab baseline

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.

redis-cli · smallest KEYS/ARGV proof
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.
Wrong approach

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.

redis-cli · bounded reservation script with EVAL
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.

Python · load once, execute by digest, verify cache
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.

Python · demonstrate safe NOSCRIPT fallback without flushing shared cache
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.
Why the lab does not call SCRIPT FLUSH

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.

redis-cli · cache evidence
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.
Repair dynamic scripts

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.

redis-cli · explicit result shape
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.

redis-cli · record server and lab context
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.
redis-cli · deterministic fixture
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

redis-cli · bounded Chapter 14 cleanup
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

  1. Why must key names go in KEYS rather than ARGV?
  2. What does EVALSHA save compared with EVAL?
  3. What should a client do on NOSCRIPT?
  4. Why is a unique script body per order an anti-pattern?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.