How Redis Functions differ from Lua scripts — functions persist across restarts in RDB/AOF. Function libraries, the shebang declaration, registering multiple functions in one library, replication semantics, and migration from EVALSHA.
A-3 — Redis Functions: Persistent Stored Procedures
Who this module is for: You use EVALSHA to call Lua scripts by SHA digest and manage script lifecycle manually — re-loading scripts after restarts, distributing SHAs across application instances. Redis Functions (introduced in Redis 7.0) solve exactly these pain points. This module covers the Functions model, how it differs from EVAL/EVALSHA, and when to migrate.
The Problem with EVALSHA
SCRIPT LOAD + EVALSHA works, but has operational friction:
-
Scripts do not persist — they live in a volatile in-memory cache. Redis restart = all scripts gone. Your application must re-load scripts on every startup (or handle
NOSCRIPTerrors). -
SHA distribution — every application instance needs to know the SHA digest of every script. If you deploy a new script version, every instance must get the new SHA simultaneously.
-
No introspection — Redis cannot list what scripts are loaded.
SCRIPT EXISTS shatells you if a specific SHA is loaded, but you cannot ask "what scripts are in the cache?" -
No namespace — all scripts share one global namespace (the SHA cache). No way to group related scripts.
Redis Functions solve all four problems.
What Redis Functions Are
Redis Functions (Redis 7.0+) are named, persistent, typed server-side functions. They are stored in Redis like data — persisted to RDB/AOF, replicated to replicas, and survive restarts.
Key differences from EVAL scripts:
| Feature | EVAL / EVALSHA | Redis Functions |
|---|---|---|
| Persistence | Volatile (cache) | Durable (RDB + AOF) |
| Naming | SHA digest only | Named (library + function name) |
| Discovery | SCRIPT EXISTS sha | FUNCTION LIST |
| Engine | Lua only | Lua (+ future engines) |
| Replication | Command replicated | Library replicated |
| Namespace | Global | Per-library |
Function Library Anatomy
Functions are grouped into libraries. A library is a Lua module with a #! shebang declaring the engine and library name, plus one or more redis.register_function() calls:
lua#!lua name=mylib -- Register a rate limiter function redis.register_function('check_rate_limit', function(keys, args) local key = keys[1] local window = tonumber(args[1]) local limit = tonumber(args[2]) local now = tonumber(args[3]) local window_start = now - window redis.call('ZREMRANGEBYSCORE', key, 0, window_start) local count = redis.call('ZCARD', key) if count >= limit then return 0 end redis.call('ZADD', key, now, tostring(now) .. tostring(math.random(1000000))) redis.call('PEXPIRE', key, window) return 1 end) -- Register a conditional set function redis.register_function('set_if_greater', function(keys, args) local current = redis.call('ZSCORE', keys[1], args[1]) if current == false or tonumber(args[2]) > tonumber(current) then redis.call('ZADD', keys[1], args[2], args[1]) return 1 end return 0 end)
Loading a Library
FUNCTION LOAD [REPLACE] function-code
bash# Load from command line redis-cli FUNCTION LOAD "#!lua name=mylib\n\nredis.register_function('my_fn', function(keys, args)\n return args[1]\nend)" # Load from file redis-cli FUNCTION LOAD "$(cat mylib.lua)" # Replace existing library (update) redis-cli FUNCTION LOAD REPLACE "$(cat mylib.lua)"
In Node.js:
typescriptimport fs from 'fs'; const libraryCode = fs.readFileSync('./src/redis/mylib.lua', 'utf8'); async function loadFunctions() { try { await redis.call('FUNCTION', 'LOAD', libraryCode); console.log('Functions loaded'); } catch (err: any) { if (err.message.includes('Library already exists')) { // Already loaded — skip (or use REPLACE to update) return; } throw err; } } // Call once at application startup await loadFunctions();
Calling Functions: FCALL
FCALL function-name numkeys key [key ...] arg [arg ...]
FCALL_RO function-name numkeys key [key ...] → read-only variant (can run on replicas)
typescript// Call the rate limiter function const allowed = await redis.call( 'FCALL', 'check_rate_limit', 1, // numkeys `rate:${userId}`, // KEYS[1] '60000', // ARGV[1]: window ms '100', // ARGV[2]: limit String(Date.now()) // ARGV[3]: current time ); // Call the conditional set const updated = await redis.call( 'FCALL', 'set_if_greater', 1, 'leaderboard:season:1', 'user:1001', // member '9500' // new score );
Managing Libraries
FUNCTION LIST [LIBRARYNAME pattern] [WITHCODE] → list all libraries (optionally with source)
FUNCTION INFO libraryname → info about a specific library
FUNCTION DELETE libraryname → remove a library
FUNCTION STATS → currently executing function (if any)
FUNCTION DUMP → serialize all libraries to RDB format
FUNCTION RESTORE payload [FLUSH | APPEND | REPLACE] → restore libraries from DUMP
FUNCTION FLUSH [ASYNC | SYNC] → delete all libraries
127.0.0.1:6379> FUNCTION LIST
1) 1) "library_name"
2) "mylib"
3) "engine"
4) "LUA"
5) "functions"
6) 1) 1) "name"
2) "check_rate_limit"
3) "description"
4) ""
5) "flags"
6) (empty array)
2) 1) "name"
2) "set_if_greater"
Function Flags
Register functions with flags to declare their behaviour:
luaredis.register_function{ function_name = 'read_session', callback = function(keys, args) return redis.call('HGETALL', keys[1]) end, flags = {'no-writes'} -- declare: this function never writes }
Available flags:
no-writes— function is read-only; can be called withFCALL_ROand executed on replicasallow-stale— allow calling on replicas even when replica is in stale stateno-cluster— function cannot run in Cluster modeallow-busy— allow calling duringlua-time-limitexceeded state (use with care)
no-writes is the most important: it enables calling the function via FCALL_RO on read replicas, reducing primary load.
Persistence and Replication
Unlike EVAL scripts, Functions are part of the Redis dataset:
- Stored in RDB snapshots (survives restart)
- Logged to AOF (survives restart with AOF persistence)
- Replicated to all replicas automatically when a library is loaded or deleted
When a replica becomes a primary (after Sentinel failover), its function library is already up-to-date — no manual re-loading required.
Deployment Workflow
Initial Deploy
bash# Load function library as part of deployment pipeline redis-cli -h redis.internal FUNCTION LOAD "$(cat src/redis/mylib.lua)"
Update Existing Library
bash# Use REPLACE to update in place (atomic replacement) redis-cli -h redis.internal FUNCTION LOAD REPLACE "$(cat src/redis/mylib.lua)"
FUNCTION LOAD REPLACE atomically replaces the old library with the new one. Inflight calls to old function names complete normally; new calls after the replacement use the new code.
Multi-Region / Multi-Instance
Since Functions are replicated to all replicas, you only need to load to the primary. All replicas receive the library automatically.
For multiple independent Redis instances (not replicas), load to each:
bashfor host in redis-1 redis-2 redis-3; do redis-cli -h $host FUNCTION LOAD REPLACE "$(cat mylib.lua)" done
Backup and Restore
bash# Backup all function libraries redis-cli FUNCTION DUMP > functions.rdb # Restore on a new instance redis-cli FUNCTION RESTORE < functions.rdb
EVAL vs Functions: When to Use Which
| Scenario | Recommendation |
|---|---|
| Redis < 7.0 | EVAL / EVALSHA (no choice) |
| Redis >= 7.0, new project | Functions — better operational model |
| Quick one-off script | EVAL — no need to manage a library |
| Production atomic operations | Functions — persistence, naming, discoverability |
| Cross-service shared logic | Functions — load once, call by name |
| Frequent deployments with script changes | Functions with REPLACE — atomic updates |
Summary
- Redis Functions (7.0+) are persistent, named, replicated server-side functions — the production-grade evolution of EVAL
- Libraries group related functions with a
#!lua name=libnameheader +redis.register_function()calls FUNCTION LOADloads a library (survives restart);FCALLcalls a function by nameFUNCTION LOAD REPLACEatomically updates an existing library — zero-downtime function updatesFCALL_RO+no-writesflag allows read-only functions to run on replicas, reducing primary load- Functions are replicated to all replicas automatically — no manual re-loading after failover
FUNCTION LIST,FUNCTION DUMP/RESTOREfor discovery and backup- Use Functions for production atomic operations in Redis 7.0+; use EVAL for quick scripts or older Redis versions
Next: A-4 — Redlock: The Algorithm, Its Guarantees, and Its Critics — the multi-instance distributed lock, what it guarantees under bounded clock drift, Martin Kleppmann's critique, and when to use it versus alternatives.