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.

Advanced170–230 minutesACLs, read-only execution, and rolloutRedis Open Source 8.10.1redis-py 8.1.0 where usedFree/local-firstLast reviewed: September 6, 2026

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.

01

Distinguish redis.call from redis.pcall and explicit redis.error_reply handling inside Lua.

02

Use no-writes functions with FCALL_RO/EVAL_RO and explain what read-only execution guarantees and restricts.

03

Prove ACL allow/deny behavior with ACL DRYRUN and a temporary least-privilege Chapter 14 user.

04

Version and inspect libraries before rollout, preserving a concrete rollback path.

05

Avoid broad +@all permissions, hidden key access, and admin-only deployment assumptions.

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. 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.

redis-cli · redis.call propagates WRONGTYPE
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.
redis-cli · redis.pcall maps the failure deliberately
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 · library with read-only and write functions
#!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'}
Python · load the library
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))
redis-cli · invoke the read-only function correctly
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.

redis-cli · deliberate read-only mismatch
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.

redis-cli · prove the existing application user cannot just script as admin
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.

redis-cli · temporary Chapter 14 user
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.
Production rule

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.

Lua · optional ACL-aware branch
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.

Topology boundary

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.
redis-cli · bounded cleanup including temporary user
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

  1. What is the main difference between redis.call and redis.pcall?
  2. What must be true for FCALL_RO?
  3. Does @scripting permission automatically authorize every command inside a Function?
  4. Why use ACL DRYRUN before rollout?
  5. 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

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.