Chapter 14 · Lua Scripting and Redis Functions
Redis Functions: Libraries, FCALL, Persistence, Deployment, and Versioned Server Logic
Package small server-side logic as named Redis Function libraries, invoke it with FCALL, inspect and back it up, and roll versions without confusing persisted Functions with the volatile script cache.
Learning outcomes
AtlasMart has a reservation routine worth deploying consistently
instead of shipping ad-hoc Lua source from every application
process. Redis Functions, available since Redis 7, package
functions into named libraries that are loaded into the server,
persisted with Redis data, replicated, inspected, dumped,
restored, and invoked with FCALL.
Explain library, function, engine, FCALL, and the difference between Functions persistence and Eval script-cache volatility.
Load a versioned Lua library, inspect it with FUNCTION LIST/STATS, and invoke it with declared keys and arguments.
Back up and restore Function libraries with FUNCTION DUMP/RESTORE using a binary-safe client path.
Use whole-library replacement/version naming deliberately rather than editing one function in place.
Separate standalone persistence evidence from later replication/failover topology verification.
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. Functions move code lifecycle onto the Redis server
An Eval script belongs to the application and may disappear from the script cache at any time. A Redis Function belongs to a named library loaded into Redis. Libraries are persisted in RDB/AOF and replicated with server state. This makes Functions a better fit for stable, explicitly deployed server-side APIs—but it also creates a deployment artifact that must be versioned, reviewed, backed up, and rolled out like code.
| Lifecycle concern | EVAL/EVALSHA | Redis Function |
|---|---|---|
| Name/version | Application convention + SHA1 | Named library + named functions |
| Presence | Cache can miss/evict/restart away | Loaded server state |
| Persistence | No | Yes, with Redis data |
| Update unit | Application source changes | Whole library is immutable; replace/load new library |
| Invocation | EVAL/EVALSHA | FCALL/FCALL_RO |
2. A library starts with a shebang and registered functions
The first line declares the Lua engine and library name.
Functions are registered with
redis.register_function. Unlike Eval scripts,
Functions receive keys and
args callback arrays rather than relying on global
KEYS/ARGV. Keep the callback small and
make all Redis keys invocation inputs.
#!lua name=atlasmart_ch14_v1redis.register_function{function_name='reserve_stock_v1', callback=function(keys,args) local qty=tonumber(args[1]); local stock=tonumber(redis.call('GET',keys[1]) or '0'); if stock < qty then return {0,stock}; end; redis.call('DECRBY',keys[1],qty); redis.call('HSET',keys[2],'status','reserved','qty',qty,'logicVersion','v1'); return {1,stock-qty}; end, description='AtlasMart bounded reservation v1'}
3. Load and inspect the library with redis-py
FUNCTION LOAD accepts the library source as one
payload. Using redis-py avoids cross-shell quoting problems and
keeps the exact source version visible in the lab file.
import redisr = redis.Redis(host="127.0.0.1", port=6379, username="academy-admin", password="AtlasMart-Admin-Lab-Only-2026", decode_responses=True)library = "#!lua name=atlasmart_ch14_v1\nredis.register_function{function_name='reserve_stock_v1', callback=function(keys,args) local qty=tonumber(args[1]); local stock=tonumber(redis.call('GET',keys[1]) or '0'); if stock < qty then return {0,stock}; end; redis.call('DECRBY',keys[1],qty); redis.call('HSET',keys[2],'status','reserved','qty',qty,'logicVersion','v1'); return {1,stock-qty}; end, description='AtlasMart bounded reservation v1'}"try: r.function_delete("atlasmart_ch14_v1")except redis.ResponseError: passprint(r.function_load(library))print(r.function_list(libraryname="atlasmart_ch14_v1", withcode=True))print(r.function_stats())
FUNCTION LIST WITHCODE exposes library metadata and
source; FUNCTION STATS exposes engine/runtime
information and a currently running function when one exists.
Treat this as deployment evidence, not application business
state.
4. FCALL invokes a named function with declared keys
FCALL function numkeys key... arg... looks similar
to EVAL's input shape because Cluster routing still needs
declared key names. The function name is global on the server,
so versioning the function name avoids silently changing callers
that still depend on v1 behavior.
SET atlasmart:ch14:l3:stock 3FCALL reserve_stock_v1 2 atlasmart:ch14:l3:stock atlasmart:ch14:l3:order:9101 2GET atlasmart:ch14:l3:stockHGETALL atlasmart:ch14:l3:order:9101# Expected: FCALL returns [1,1]; order hash contains logicVersion=v1.
5. FUNCTION DUMP/RESTORE is binary deployment evidence
FUNCTION DUMP returns a serialized binary payload
for all loaded libraries. Do not round-trip it through a text
editor or a decode/encode path. The lab uses redis-py bytes,
deletes only the Chapter 14 v1 library, restores the payload
with an explicit policy, and verifies the library is back.
import redisr = redis.Redis(host="127.0.0.1", port=6379, username="academy-admin", password="AtlasMart-Admin-Lab-Only-2026", decode_responses=False)payload = r.function_dump()print("dump_bytes", len(payload), "sha256", __import__("hashlib").sha256(payload).hexdigest())r.function_delete("atlasmart_ch14_v1")print("after-delete", r.function_list(libraryname="atlasmart_ch14_v1"))r.function_restore(payload, policy="APPEND")print("after-restore", r.function_list(libraryname="atlasmart_ch14_v1"))
FUNCTION DUMP serializes all loaded Function libraries, not just the AtlasMart one. On a shared server, restoring a full dump can interact with other libraries. This lab assumes the disposable Chapter 01 container; production needs inventory, collision policy, backup isolation, and restore drills.
6. Restart comparison: Functions persist; Eval cache does not
The clearest local lifecycle test is a controlled restart of the
disposable container. First load a harmless Eval script and the
Function library, record both, restart Redis, then compare
SCRIPT EXISTS with FUNCTION LIST.
Because the lab uses AOF/RDB and a persistent volume, the
Function library should reload with the dataset while the Eval
cache is not a persistence contract.
import redisr = redis.Redis(host="127.0.0.1", port=6379, username="academy-admin", password="AtlasMart-Admin-Lab-Only-2026", decode_responses=True)sha = r.script_load("return 14")print("eval_sha", sha, "exists", r.script_exists(sha))print("functions", r.function_list(libraryname="atlasmart_ch14_v1"))print("Save this SHA, then run the bounded container restart below.")
docker restart atlasmart-redis-ch01# Reconnect after the container reports healthy/running. This interrupts only the dedicated learning instance.
FUNCTION LIST LIBRARYNAME atlasmart_ch14_v1SCRIPT EXISTS <the-SHA-recorded-before-restart># Expect the Function library to remain if persistence loaded successfully. Do not promise script-cache survival.
7. Version libraries instead of mutating callers invisibly
Function libraries are immutable as loaded units; changes are
deployed as a whole. A safe migration can load
atlasmart_ch14_v2 beside v1, expose
reserve_stock_v2, move callers deliberately, and
delete v1 only after rollback windows close.
FUNCTION LOAD REPLACE is useful when replacing the
same library name is truly intended, but it should not become an
excuse for invisible API mutation.
#!lua name=atlasmart_ch14_v2redis.register_function{function_name='reserve_stock_v2', callback=function(keys,args) local qty=tonumber(args[1]); if not qty or qty <= 0 then return redis.error_reply('ERR qty must be positive'); end; local stock=tonumber(redis.call('GET',keys[1]) or '0'); if stock < qty then return {0,stock}; end; redis.call('DECRBY',keys[1],qty); redis.call('HSET',keys[2],'status','reserved','qty',qty,'logicVersion','v2'); return {1,stock-qty}; end, description='AtlasMart bounded reservation v2 with qty validation'}
import redisr = redis.Redis(host="127.0.0.1", port=6379, username="academy-admin", password="AtlasMart-Admin-Lab-Only-2026", decode_responses=True)library_v2 = "#!lua name=atlasmart_ch14_v2\nredis.register_function{function_name='reserve_stock_v2', callback=function(keys,args) local qty=tonumber(args[1]); if not qty or qty <= 0 then return redis.error_reply('ERR qty must be positive'); end; local stock=tonumber(redis.call('GET',keys[1]) or '0'); if stock < qty then return {0,stock}; end; redis.call('DECRBY',keys[1],qty); redis.call('HSET',keys[2],'status','reserved','qty',qty,'logicVersion','v2'); return {1,stock-qty}; end, description='AtlasMart bounded reservation v2 with qty validation'}"print(r.function_load(library_v2))print(r.function_list(libraryname="atlasmart_ch14_v2", withcode=True))
8. Replication/failover claims require the later topology labs
Official Redis documentation states that Function libraries are replicated and persisted, but this mandatory Chapter 14 topology is standalone. Therefore the chapter can verify persistence across restart and serialized dump/restore locally, but it does not fabricate replica promotion evidence. Chapters 19–20 will add real primary/replica and Sentinel failure tests with the same function-library lifecycle questions.
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.
9. Verification checklist, cleanup, and production judgment
- FUNCTION LOAD created a named v1 library and FUNCTION LIST WITHCODE showed its exact source.
- FCALL executed reserve_stock_v1 against only declared lesson keys.
- FUNCTION DUMP produced binary bytes and the restore path verified library recovery.
- A controlled restart distinguished persisted Function state from volatile Eval cache assumptions.
- v2 was loaded as a separate versioned API rather than silently changing v1 callers.
UNLINK atlasmart:ch14:l3:stock atlasmart:ch14:l3:order:9101FUNCTION DELETE atlasmart_ch14_v1FUNCTION DELETE atlasmart_ch14_v2
Use Functions for small stable server APIs whose deployment lifecycle matters. Treat library source, names, flags, ACL requirements, RDB/AOF compatibility, restore artifacts, and rollback plans as operational code. Do not move large domain services into Redis simply because Functions persist.
Check your understanding
- What makes Functions operationally different from EVALSHA?
- What is the update unit for a Function library?
- Why use a binary-safe client for FUNCTION DUMP?
- Did the restart test prove Sentinel failover?
- Why keep v1 and v2 function names during migration?
Review the answers
Functions are named server libraries persisted with Redis state and replicated; EVALSHA depends on an ephemeral script cache.
The whole library, not one function edited independently.
The dump is serialized binary data; text transformations can corrupt it.
No. It proves local persistence/reload behavior only; failover requires a replicated topology.
It makes caller compatibility and rollback explicit instead of silently changing the server API.
10. Summary and next step
You now have a deployable, persisted, inspectable Redis Function library and a versioned rollout model. Lesson 4 adds error handling, read-only flags/commands, ACL proof, and rollout guardrails so “it works as admin” does not become the security model.
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