migration in practice
Classic Lua scripting with EVAL and EVALSHA has been the standard way to run atomic server logic in Redis for years, but it brings practical problems around maintainability, versioning, and replication behavior. Since Redis 7, Redis Functions offer a more structured alternative through FUNCTION LOAD, treating scripts as named, persistently stored libraries instead of ephemeral, hash-referenced individual scripts. What the switch looks like in practice, what advantages it brings, and where EVAL still remains the better choice, is what this article covers.
Table of Contents
- 1. Why classic EVAL hits limits in practice
- 2. The basic principle of Redis Functions: named libraries instead of anonymous scripts
- 3. Loading a library and calling functions
- 4. Maintainability advantage: named libraries instead of a hash zoo
- 5. Replication advantage: effect replication instead of script propagation
- 6. Practical migration path: converting an existing EVAL script
- 7. When EVAL still remains the better choice
- 8. Extra control: flags for read access and timing behavior
- 9. Operational management: listing, deleting, and persisting libraries
- 10. Summary
- 11. FAQ
1. Why classic EVAL hits limits in practice
A Lua script executed via EVAL initially exists for Redis merely as an anonymous block of text, which would have to be sent along with every call, were it not for server-side script caching via SCRIPT LOAD and the subsequent invocation by SHA1 hash with EVALSHA. This model works, but carries a structural problem: the hash is a pure identifier with no semantic meaning, so application code usually has to keep its own bookkeeping of which hash belongs to which script, and after a server restart or SCRIPT FLUSH, all scripts must be reloaded before EVALSHA works again.
There is also no built-in versioning: if a script changes, its hash changes too, and old hashes still in circulation become invalid, without Redis itself keeping any overview of which script versions are actually in use at a given time. For a handful of small, rarely changed scripts this is not much of a problem, but for a growing collection of long-lived server logic it quickly becomes unwieldy.
2. The basic principle of Redis Functions: named libraries instead of anonymous scripts
Redis Functions flip this model around: instead of individual, hash-referenced scripts, FUNCTION LOAD loads a complete, named library containing one or more named functions. Each function is called by its name, for instance with FCALL myfunction 1 mykey, not via a hash computed from the script content. The library name itself is part of Redis's persistent configuration and thus survives restarts, without the application having to keep its own bookkeeping of hashes.
Technically, Lua remains the scripting language; Functions are not a new programming language, but a more structured wrapper around the same Lua execution environment, complemented by a fixed registration scheme via redis.register_function() within the library definition.
#!lua name=cart_library
redis.register_function('cart_add_item', function(keys, args)
local cart_key = keys[1]
local item_id = args[1]
local qty = tonumber(args[2])
redis.call('HINCRBY', cart_key, item_id, qty)
redis.call('EXPIRE', cart_key, 3600)
return redis.call('HGETALL', cart_key)
end)
3. Loading a library and calling functions
Loading a library happens via FUNCTION LOAD, followed by the complete library source, which must start with a shebang line #!lua name=libraryname. This name binding is mandatory and ensures Redis immediately detects naming conflicts between different libraries, instead of letting them surface later as hard-to-diagnose runtime errors.
Calling a registered function happens via FCALL or, for pure read functions without write access, via FCALL_RO, each with the function name, the number of keys passed, the keys themselves, and any additional arguments. This explicit separation between keys and other arguments matches the familiar pattern from EVAL, so existing call conventions carry over largely unchanged.
# Load a library from a file
redis-cli -x FUNCTION LOAD < cart_library.lua
# Call a registered function: 1 key, two arguments
FCALL cart_add_item 1 cart:4711 sku-123 2
# List all loaded libraries and their functions
FUNCTION LIST
4. Maintainability advantage: named libraries instead of a hash zoo
The central maintainability advantage of Functions is that a library gets swapped as a whole: FUNCTION LOAD REPLACE fully replaces an existing library with a new version, while every function name it contains stays stable as long as it is still defined in the new version. Application code that calls functions by name therefore does not need to change on an update, unlike EVALSHA calls, which must reference a new hash after every script change.
In addition, the complete library source can be backed up at any time via FUNCTION DUMP and restored via FUNCTION RESTORE, enabling clean version management outside Redis, for instance as part of a deployment repository where every library version exists as its own traceable file, instead of a collection of hard-to-attribute hash values.
5. Replication advantage: effect replication instead of script propagation
With classic EVAL, Redis by default propagates the actually executed, resulting write commands to replicas and to the AOF, not the script call itself, as long as the script is marked deterministic. For non-deterministic scripts, such as ones accessing TIME or RANDOMKEY, the script author must explicitly switch to effect replication via redis.replicate_commands(), which is often forgotten in older scripts and can then lead to inconsistencies between primary and replica instances.
Redis Functions enforce effect replication by default from the outset, without the function author having to worry about it explicitly. Every write operation executed within a Function is propagated to replicas individually and deterministically, structurally eliminating the risk of silent inconsistencies from forgotten replication instructions, rather than leaving it to the script author's discipline.
6. Practical migration path: converting an existing EVAL script
Migrating an existing EVAL script starts with identifying all scripts in production use, usually via SCRIPT LIST or an inventory in the application code, since Redis itself keeps no description of what a script is meant for. Next, the existing Lua code gets wrapped in a function definition with a shebang line and a redis.register_function() call, where the actual logic can usually be carried over unchanged, since the underlying Lua execution environment stays the same.
The application is then prepared to call both paths in parallel, calling both EVALSHA and FCALL on a trial basis, comparing results in a staging environment, and only switching fully to FCALL after successful verification. Old, no-longer-needed EVAL-based scripts can then be removed from the application code step by step, without requiring a hard cutover point.
-- Before: classic EVAL script (atomic counter with limit)
-- KEYS[1] = counter key, ARGV[1] = max value
if tonumber(redis.call('GET', KEYS[1]) or '0') >= tonumber(ARGV[1]) then
return 0
end
return redis.call('INCR', KEYS[1])
-- After: the same logic as a registered function
#!lua name=counter_library
redis.register_function('counter_incr_limited', function(keys, args)
local current = tonumber(redis.call('GET', keys[1]) or '0')
if current >= tonumber(args[1]) then
return 0
end
return redis.call('INCR', keys[1])
end)
7. When EVAL still remains the better choice
For one-off, ad-hoc scripts, such as a manual data migration run as part of a single maintenance action or a debugging script that only runs temporarily during an incident investigation, the extra overhead of a named, registered library is disproportionate. Here, EVAL with the script content sent directly remains the more pragmatic choice, since no persistent registration or library cleanup is required.
In very old Redis versions before 7.0, still running in some production environments for compatibility reasons, FUNCTION LOAD simply is not available, so EVAL remains the only option there for lack of an alternative. When a Redis 7 or newer upgrade is planned, it is worth deliberately deciding which existing scripts genuinely benefit from migrating to Functions and which are better left as one-off tools on EVAL.
8. Extra control: flags for read access and timing behavior
When registering a function, additional properties can be declared through a flags table, such as no-writes for functions guaranteed not to execute any write commands and therefore safely callable via FCALL_RO on replicas, or allow-stale for functions that can run meaningfully even on a replica with stale data. These flags are enforced by Redis itself; a function call carrying the no-writes flag that nevertheless contains a write command is rejected with an error.
Classic EVAL has no comparable declarative safeguard: whether a script only reads is something the caller must know and ensure by convention, without Redis technically enforcing it. This extra layer of control is another, often underestimated maintainability advantage of Functions over EVAL, especially in teams with multiple developers maintaining the same library.
#!lua name=readonly_library
redis.register_function{
function_name='cart_get_total',
callback=function(keys, args)
local items = redis.call('HGETALL', keys[1])
local total = 0
for i = 2, #items, 2 do
total = total + tonumber(items[i])
end
return total
end,
flags={'no-writes'}
}
9. Operational management: listing, deleting, and persisting libraries
Loaded libraries are treated as persistent by default: they survive a restart as long as RDB or AOF persistence is active, and are automatically transferred to replicas during replication, without the application having to load them there again. FUNCTION LIST WITHCODE retrieves the complete source of all loaded libraries along with metadata such as function names and flags, which works well for automated consistency checks between the expected and the actually loaded version.
Libraries no longer needed can be removed specifically with FUNCTION DELETE libraryname without affecting other libraries, while FUNCTION FLUSH deletes all loaded libraries at once and should therefore only be used deliberately and rarely, for instance as part of a controlled rollback.
| Aspect | Classic EVAL/EVALSHA | Redis Functions | Practical relevance |
|---|---|---|---|
| Identification | SHA1 hash of the script content | Named library and function | Functions need no manual hash bookkeeping |
| Update behavior | New hash on every change | FUNCTION LOAD REPLACE, name stays stable | Calling code stays unchanged with Functions |
| Replication | Effect replication optional, must be enabled | Effect replication on by default | Functions structurally more consistent |
| Declarative flags | Not available | no-writes, allow-stale, and more | Functions enforce safeguards technically |
| Best suited for | One-off ad-hoc scripts | Long-lived server logic | Choose based on the script's expected lifetime |
Mironsoft
Cache layer setup and Magento Redis integration
Magento cache that isn't quite working or is misconfigured?
We set up Redis as a cache and session backend for Magento cleanly, tune memory usage and eviction strategies, and make sure full page cache and session storage work together reliably.
Redis Setup
Configure the cache, session, and FPC backend production-ready for Magento.
Memory Tuning
Match memory usage and eviction policies to the shop's actual load.
High Availability Setup
Set up Redis Sentinel or Cluster for resilient Magento environments.
10. Summary
Redis Functions vs. Lua Scripting: The Essentials at a Glance
Core difference
EVAL references anonymous scripts via hashes, Functions load named libraries via FUNCTION LOAD with stable function names.
Maintainability advantage
FUNCTION LOAD REPLACE swaps a library as a whole, without needing any changes to calling code.
Replication advantage
Functions enforce effect replication by default, EVAL requires an explicit redis.replicate_commands() call for that.
Migration strategy
Convert existing scripts into registered functions step by step, test them in parallel, and switch over fully only after verification.