Chapter 14 · Lua Scripting and Redis Functions
Error Handling, Read-Only Functions/Scripts, ACL Interaction, and Safe Rollout
Handle Lua/Redis errors deliberately, use read-only execution correctly, prove ACL allow/deny behavior, and stage Function changes with a rollback path instead of broad privileges or blind replacement.
Learning outcomes
Server-side logic that works under
academy-admin can still be unsafe to deploy.
AtlasMart needs predictable error replies, an explicit read-only
API, least-privilege invocation, and a rollout that can be
checked before traffic moves. This lesson treats errors and ACLs
as part of the function contract rather than post-deployment
surprises.
Distinguish redis.call from redis.pcall and explicit redis.error_reply handling inside Lua.
Use no-writes functions with FCALL_RO/EVAL_RO and explain what read-only execution guarantees and restricts.
Prove ACL allow/deny behavior with ACL DRYRUN and a temporary least-privilege Chapter 14 user.
Version and inspect libraries before rollout, preserving a concrete rollback path.
Avoid broad +@all permissions, hidden key access, and admin-only deployment assumptions.
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. redis.call propagates errors; redis.pcall lets Lua inspect them
redis.call() raises a Redis/Lua error back to the
caller when the invoked command fails.
redis.pcall() returns an error reply object to Lua
so the script/function can translate or handle the problem.
Handling an error does not mean ignoring it: application
contracts should preserve enough detail to distinguish
validation, missing state, authorization, and infrastructure
failures.
SET atlasmart:ch14:l4:wrongtype textEVAL "return redis.call('HGET',KEYS[1],'status')" 1 atlasmart:ch14:l4:wrongtype# Expected: script error; the String was not silently coerced to a Hash.
EVAL "local r=redis.pcall('HGET',KEYS[1],'status'); if r.err then return redis.error_reply('ERR atlasmart status unavailable') end; return r" 1 atlasmart:ch14:l4:wrongtype# Expected: controlled application-facing error reply.
2. Read-only is an execution contract, not a comment
A function registered with the no-writes flag can
be invoked through FCALL_RO. Redis enforces that
the function does not execute write commands. Likewise,
EVAL_RO/EVALSHA_RO reject writes. This
enables read-only execution on replicas and under conditions
where normal writes are refused, but it does not make
stale-replica data magically current.
#!lua name=atlasmart_ch14_safe_v1local function get_status(keys,args) return redis.call('HGET',keys[1],'status') endlocal function guarded_qty(keys,args) local qty=tonumber(args[1]); if not qty or qty <= 0 then return redis.error_reply('ERR qty must be positive'); end; return redis.call('DECRBY',keys[1],qty) endredis.register_function{function_name='get_status_v1', callback=get_status, flags={'no-writes'}, description='Read AtlasMart status'}redis.register_function{function_name='guarded_qty_v1', callback=guarded_qty, description='Validated inventory decrement'}
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_safe_v1\nlocal function get_status(keys,args) return redis.call('HGET',keys[1],'status') end\nlocal function guarded_qty(keys,args) local qty=tonumber(args[1]); if not qty or qty <= 0 then return redis.error_reply('ERR qty must be positive'); end; return redis.call('DECRBY',keys[1],qty) end\nredis.register_function{function_name='get_status_v1', callback=get_status, flags={'no-writes'}, description='Read AtlasMart status'}\nredis.register_function{function_name='guarded_qty_v1', callback=guarded_qty, description='Validated inventory decrement'}"try: r.function_delete("atlasmart_ch14_safe_v1")except redis.ResponseError: passprint(r.function_load(library))
HSET atlasmart:ch14:l4:order:9201 status packedFCALL_RO get_status_v1 1 atlasmart:ch14:l4:order:9201# Expected: "packed".
3. FCALL_RO rejects a write-capable function
FCALL_RO is not a way to “try” a normal Function
against a replica. The target function must be declared
read-only and must not issue writes. The write function remains
callable with FCALL when the caller has permission
and the server accepts writes.
SET atlasmart:ch14:l4:stock 3FCALL_RO guarded_qty_v1 1 atlasmart:ch14:l4:stock 1# Expected: error because guarded_qty_v1 is not a read-only function.FCALL guarded_qty_v1 1 atlasmart:ch14:l4:stock 1GET atlasmart:ch14:l4:stock# Expected after FCALL: 2.
4. ACLs apply to invocation and commands used inside Lua
EVAL/FCALL are in the @scripting ACL category, but
permission to invoke scripting is not sufficient if the
executing user lacks access to the keys or Redis commands the
code calls. Redis's Lua API exposes
redis.acl_check_cmd() for advanced preflight logic,
while operators can use ACL DRYRUN to test a user
without causing side effects.
ACL DRYRUN atlasmart-app FCALL get_status_v1 1 atlasmart:ch14:l4:order:9201ACL DRYRUN atlasmart-app EVAL "return redis.call('GET',KEYS[1])" 1 atlasmart:ch14:l4:stock# Expect denial if the Chapter 01 atlasmart-app user has no @scripting permission.
5. Create one temporary least-privilege scripting user
The lab uses a dedicated temporary user instead of widening
atlasmart-app. It grants scripting plus only the
command families and key prefix required by this chapter, then
verifies both an allowed and denied path. Because the user
exists only in the disposable lab, cleanup deletes it
explicitly.
ACL SETUSER atlasmart-ch14-script on >AtlasMart-Ch14-Script-Lab-Only-2026 resetkeys resetchannels -@all +@connection +@read +@write +@scripting ~atlasmart:ch14:*ACL DRYRUN atlasmart-ch14-script FCALL get_status_v1 1 atlasmart:ch14:l4:order:9201ACL DRYRUN atlasmart-ch14-script CONFIG GET maxmemory# First should be allowed if underlying commands/key access also fit; CONFIG should remain denied.
Do not grant +@all merely because a Function
failed. Inspect the Function commands/keys and grant the
smallest required permissions. Administration commands for
loading/deleting libraries should normally remain separate
from application invocation identities.
6. The current user is enforced inside redis.call
Lua does not become root inside Redis. Commands called through
redis.call()/redis.pcall() are checked
against the authenticated user's ACL permissions and key
patterns. This is why a function can be visible yet still fail
for an application identity if it attempts a disallowed
operation.
if not redis.acl_check_cmd('HGET', keys[1], 'status') then return redis.error_reply('ERR caller cannot read status')endreturn redis.call('HGET', keys[1], 'status')
7. Safe rollout: inventory, load, smoke-test, switch, then remove
A Function deployment should be observable before clients depend
on it. Record FUNCTION LIST WITHCODE, hash the
exact source in CI/deployment tooling, load a versioned library,
smoke-test through the same ACL identity callers will use,
observe errors/latency, and preserve the old version until the
rollback window closes.
| Stage | Evidence | Rollback |
|---|---|---|
| Inventory | FUNCTION LIST WITHCODE / expected source hash | No mutation yet |
| Load | New library/function appears | FUNCTION DELETE new library |
| Smoke test | FCALL/FCALL_RO through app ACL | Keep old callers on v1 |
| Traffic switch | App metrics/errors/latency | Route callers back to old function |
| Retire | No callers on old version | Re-load from source/dump if rollback still supported |
8. Read-only does not mean “consistent everywhere”
FCALL_RO can run a no-writes function on replicas,
but replica reads can be stale because Redis replication is
asynchronous. A read-only function that computes order status
from a lagging replica may return internally consistent Lua
output that is still older than the primary. Mark
topology/freshness requirements separately from the function's
write flag.
The Chapter 14 mandatory lab is standalone. It proves command/read-only enforcement, not replica freshness. Actual replica behavior belongs in Chapter 19.
9. Verification checklist and cleanup
- redis.call and redis.pcall produced visibly different error-handling behavior.
- get_status_v1 worked with FCALL_RO, while the write function was rejected by FCALL_RO.
- ACL DRYRUN proved existing/default application permissions instead of guessing.
- The temporary scripting user could not run unrelated CONFIG administration.
- The rollout plan keeps old and new versions distinct and testable.
UNLINK atlasmart:ch14:l4:wrongtype atlasmart:ch14:l4:order:9201 atlasmart:ch14:l4:stockFUNCTION DELETE atlasmart_ch14_safe_v1ACL DELUSER atlasmart-ch14-script
In production, separate code deployment identity from application invocation identity, test Functions under least privilege, use read-only flags/commands only when the implementation is truly no-write, and track errors plus p95/p99 latency after rollout. Read-only execution is not a substitute for replication freshness policy.
Check your understanding
- What is the main difference between redis.call and redis.pcall?
- What must be true for FCALL_RO?
- Does @scripting permission automatically authorize every command inside a Function?
- Why use ACL DRYRUN before rollout?
- Can FCALL_RO on a replica guarantee fresh data?
Review the answers
redis.call raises a runtime error out of Lua; redis.pcall returns an error reply object that Lua can inspect/translate.
The target function must be registered as no-writes and must not execute write commands.
No. The authenticated user still needs the relevant command and key permissions.
It verifies a user/command permission path without executing the side effect.
No. Read-only execution and replica freshness are separate concerns.
10. Summary and next step
Functions are only production-ready when error shape, read/write flags, ACL permissions, and rollout/rollback are explicit. Lesson 5 applies those controls to a race-prone AtlasMart workflow and measures whether one small Function actually improves correctness and round-trip cost under concurrency.
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